Simple tower defense tutorial, part 16: Adding new enemey types.
Originally I wanted to make this a post about balancing the game, but there is still a lack of enemies and rules to provide a good challenge. So, let's add some enemies first that have some special rules.
- ENEMY_TYPE_MINION - basic enemy we have right now. No special rules, just has hitpoints.
- ENEMY_TYPE_RUNNER - faster than the minion, but has less hitpoints.
- ENEMY_TYPE_SHIELD - enemy with a shield that does damage reduction. The shield has hitpoints and can be destroyed, which converts the enemy to a shieldless unit.
- ENEMY_TYPE_BOSS - a big enemy with a lot of hitpoints.
The first change we do is to add new ids to the game as well as defining a weapon for the minion.
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
21 #define ENEMY_TYPE_MINION 1
22 #define ENEMY_TYPE_RUNNER 2
23 #define ENEMY_TYPE_SHIELD 3
24 #define ENEMY_TYPE_BOSS 3
25
26 #define PARTICLE_MAX_COUNT 400
27 #define PARTICLE_TYPE_NONE 0
28 #define PARTICLE_TYPE_EXPLOSION 1
29
30 typedef struct Particle
31 {
32 uint8_t particleType;
33 float spawnTime;
34 float lifetime;
35 Vector3 position;
36 Vector3 velocity;
37 Vector3 scale;
38 } Particle;
39
40 #define TOWER_MAX_COUNT 400
41 enum TowerType
42 {
43 TOWER_TYPE_NONE,
44 TOWER_TYPE_BASE,
45 TOWER_TYPE_ARCHER,
46 TOWER_TYPE_BALLISTA,
47 TOWER_TYPE_CATAPULT,
48 TOWER_TYPE_WALL,
49 TOWER_TYPE_COUNT
50 };
51
52 typedef struct HitEffectConfig
53 {
54 float damage;
55 float areaDamageRadius;
56 float pushbackPowerDistance;
57 } HitEffectConfig;
58
59 typedef struct TowerTypeConfig
60 {
61 float cooldown;
62 float range;
63 float projectileSpeed;
64
65 uint8_t cost;
66 uint8_t projectileType;
67 uint16_t maxHealth;
68
69 HitEffectConfig hitEffect;
70 } TowerTypeConfig;
71
72 typedef struct Tower
73 {
74 int16_t x, y;
75 uint8_t towerType;
76 Vector2 lastTargetPosition;
77 float cooldown;
78 float damage;
79 } Tower;
80
81 typedef struct GameTime
82 {
83 float time;
84 float deltaTime;
85 uint32_t frameCount;
86
87 float fixedDeltaTime;
88 // leaving the fixed time stepping to the update functions,
89 // we need to know the fixed time at the start of the frame
90 float fixedTimeStart;
91 // and the number of fixed steps that we have to make this frame
92 // The fixedTime is fixedTimeStart + n * fixedStepCount
93 uint8_t fixedStepCount;
94 } GameTime;
95
96 typedef struct ButtonState {
97 char isSelected;
98 char isDisabled;
99 } ButtonState;
100
101 typedef struct GUIState {
102 int isBlocked;
103 } GUIState;
104
105 typedef enum LevelState
106 {
107 LEVEL_STATE_NONE,
108 LEVEL_STATE_BUILDING,
109 LEVEL_STATE_BUILDING_PLACEMENT,
110 LEVEL_STATE_BATTLE,
111 LEVEL_STATE_WON_WAVE,
112 LEVEL_STATE_LOST_WAVE,
113 LEVEL_STATE_WON_LEVEL,
114 LEVEL_STATE_RESET,
115 } LevelState;
116
117 typedef struct EnemyWave {
118 uint8_t enemyType;
119 uint8_t wave;
120 uint16_t count;
121 float interval;
122 float delay;
123 Vector2 spawnPosition;
124
125 uint16_t spawned;
126 float timeToSpawnNext;
127 } EnemyWave;
128
129 #define ENEMY_MAX_WAVE_COUNT 10
130
131 typedef enum PlacementPhase
132 {
133 PLACEMENT_PHASE_STARTING,
134 PLACEMENT_PHASE_MOVING,
135 PLACEMENT_PHASE_PLACING,
136 } PlacementPhase;
137
138 typedef struct Level
139 {
140 int seed;
141 LevelState state;
142 LevelState nextState;
143 Camera3D camera;
144 int placementMode;
145 PlacementPhase placementPhase;
146 float placementTimer;
147 int16_t placementX;
148 int16_t placementY;
149 Vector2 placementTransitionPosition;
150 PhysicsPoint placementTowerSpring;
151
152 int initialGold;
153 int playerGold;
154
155 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
156 int currentWave;
157 float waveEndTimer;
158 } Level;
159
160 typedef struct DeltaSrc
161 {
162 char x, y;
163 } DeltaSrc;
164
165 typedef struct PathfindingMap
166 {
167 int width, height;
168 float scale;
169 float *distances;
170 long *towerIndex;
171 DeltaSrc *deltaSrc;
172 float maxDistance;
173 Matrix toMapSpace;
174 Matrix toWorldSpace;
175 } PathfindingMap;
176
177 // when we execute the pathfinding algorithm, we need to store the active nodes
178 // in a queue. Each node has a position, a distance from the start, and the
179 // position of the node that we came from.
180 typedef struct PathfindingNode
181 {
182 int16_t x, y, fromX, fromY;
183 float distance;
184 } PathfindingNode;
185
186 typedef struct EnemyId
187 {
188 uint16_t index;
189 uint16_t generation;
190 } EnemyId;
191
192 typedef struct EnemyClassConfig
193 {
194 float speed;
195 float health;
196 float radius;
197 float maxAcceleration;
198 float requiredContactTime;
199 float explosionDamage;
200 float explosionRange;
201 float explosionPushbackPower;
202 int goldValue;
203 } EnemyClassConfig;
204
205 typedef struct Enemy
206 {
207 int16_t currentX, currentY;
208 int16_t nextX, nextY;
209 Vector2 simPosition;
210 Vector2 simVelocity;
211 uint16_t generation;
212 float walkedDistance;
213 float startMovingTime;
214 float damage, futureDamage;
215 float contactTime;
216 uint8_t enemyType;
217 uint8_t movePathCount;
218 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
219 } Enemy;
220
221 // a unit that uses sprites to be drawn
222 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
223 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
224 typedef struct SpriteUnit
225 {
226 Rectangle srcRect;
227 Vector2 offset;
228 int frameCount;
229 float frameDuration;
230 Rectangle srcWeaponIdleRect;
231 Vector2 srcWeaponIdleOffset;
232 Rectangle srcWeaponCooldownRect;
233 Vector2 srcWeaponCooldownOffset;
234 } SpriteUnit;
235
236 #define PROJECTILE_MAX_COUNT 1200
237 #define PROJECTILE_TYPE_NONE 0
238 #define PROJECTILE_TYPE_ARROW 1
239 #define PROJECTILE_TYPE_CATAPULT 2
240 #define PROJECTILE_TYPE_BALLISTA 3
241
242 typedef struct Projectile
243 {
244 uint8_t projectileType;
245 float shootTime;
246 float arrivalTime;
247 float distance;
248 Vector3 position;
249 Vector3 target;
250 Vector3 directionNormal;
251 EnemyId targetEnemy;
252 HitEffectConfig hitEffectConfig;
253 } Projectile;
254
255 //# Function declarations
256 float TowerGetMaxHealth(Tower *tower);
257 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
258 int EnemyAddDamageRange(Vector2 position, float range, float damage);
259 int EnemyAddDamage(Enemy *enemy, float damage);
260
261 //# Enemy functions
262 void EnemyInit();
263 void EnemyDraw();
264 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
265 void EnemyUpdate();
266 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
267 float EnemyGetMaxHealth(Enemy *enemy);
268 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
269 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
270 EnemyId EnemyGetId(Enemy *enemy);
271 Enemy *EnemyTryResolve(EnemyId enemyId);
272 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
273 int EnemyAddDamage(Enemy *enemy, float damage);
274 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
275 int EnemyCount();
276 void EnemyDrawHealthbars(Camera3D camera);
277
278 //# Tower functions
279 void TowerInit();
280 Tower *TowerGetAt(int16_t x, int16_t y);
281 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
282 Tower *GetTowerByType(uint8_t towerType);
283 int GetTowerCosts(uint8_t towerType);
284 float TowerGetMaxHealth(Tower *tower);
285 void TowerDraw();
286 void TowerDrawSingle(Tower tower);
287 void TowerUpdate();
288 void TowerDrawHealthBars(Camera3D camera);
289 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
290
291 //# Particles
292 void ParticleInit();
293 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
294 void ParticleUpdate();
295 void ParticleDraw();
296
297 //# Projectiles
298 void ProjectileInit();
299 void ProjectileDraw();
300 void ProjectileUpdate();
301 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
302
303 //# Pathfinding map
304 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
305 float PathFindingGetDistance(int mapX, int mapY);
306 Vector2 PathFindingGetGradient(Vector3 world);
307 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
308 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
309 void PathFindingMapDraw();
310
311 //# UI
312 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
313
314 //# Level
315 void DrawLevelGround(Level *level);
316 void DrawEnemyPath(Level *level, Color arrowColor);
317
318 //# variables
319 extern Level *currentLevel;
320 extern Enemy enemies[ENEMY_MAX_COUNT];
321 extern int enemyCount;
322 extern EnemyClassConfig enemyClassConfigs[];
323
324 extern GUIState guiState;
325 extern GameTime gameTime;
326 extern Tower towers[TOWER_MAX_COUNT];
327 extern int towerCount;
328
329 extern Texture2D palette, spriteSheet;
330
331 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 [ENEMY_TYPE_RUNNER] = {
19 .health = 5.0f,
20 .speed = 1.0f,
21 .radius = 0.25f,
22 .maxAcceleration = 2.0f,
23 .explosionDamage = 1.0f,
24 .requiredContactTime = 0.5f,
25 .explosionRange = 1.0f,
26 .explosionPushbackPower = 0.25f,
27 .goldValue = 2,
28 },
29 };
30
31 Enemy enemies[ENEMY_MAX_COUNT];
32 int enemyCount = 0;
33
34 SpriteUnit enemySprites[] = {
35 [ENEMY_TYPE_MINION] = {
36 .srcRect = {0, 16, 16, 16},
37 .offset = {8.0f, 0.0f},
38 .frameCount = 6,
39 .frameDuration = 0.1f,
40 .srcWeaponIdleRect = {0, 32, 16, 16},
41 },
42 [ENEMY_TYPE_RUNNER] = {
43 .srcRect = {0, 16, 16, 16},
44 .offset = {8.0f, 0.0f},
45 .frameCount = 6,
46 .frameDuration = 0.1f,
47 },
48 };
49
50 void EnemyInit()
51 {
52 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
53 {
54 enemies[i] = (Enemy){0};
55 }
56 enemyCount = 0;
57 }
58
59 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
60 {
61 return enemyClassConfigs[enemy->enemyType].speed;
62 }
63
64 float EnemyGetMaxHealth(Enemy *enemy)
65 {
66 return enemyClassConfigs[enemy->enemyType].health;
67 }
68
69 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
70 {
71 int16_t castleX = 0;
72 int16_t castleY = 0;
73 int16_t dx = castleX - currentX;
74 int16_t dy = castleY - currentY;
75 if (dx == 0 && dy == 0)
76 {
77 *nextX = currentX;
78 *nextY = currentY;
79 return 1;
80 }
81 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
82
83 if (gradient.x == 0 && gradient.y == 0)
84 {
85 *nextX = currentX;
86 *nextY = currentY;
87 return 1;
88 }
89
90 if (fabsf(gradient.x) > fabsf(gradient.y))
91 {
92 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
93 *nextY = currentY;
94 return 0;
95 }
96 *nextX = currentX;
97 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
98 return 0;
99 }
100
101
102 // this function predicts the movement of the unit for the next deltaT seconds
103 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
104 {
105 const float pointReachedDistance = 0.25f;
106 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
107 const float maxSimStepTime = 0.015625f;
108
109 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
110 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
111 int16_t nextX = enemy->nextX;
112 int16_t nextY = enemy->nextY;
113 Vector2 position = enemy->simPosition;
114 int passedCount = 0;
115 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
116 {
117 float stepTime = fminf(deltaT - t, maxSimStepTime);
118 Vector2 target = (Vector2){nextX, nextY};
119 float speed = Vector2Length(*velocity);
120 // draw the target position for debugging
121 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
122 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
123 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
124 {
125 // we reached the target position, let's move to the next waypoint
126 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
127 target = (Vector2){nextX, nextY};
128 // track how many waypoints we passed
129 passedCount++;
130 }
131
132 // acceleration towards the target
133 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
134 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
135 *velocity = Vector2Add(*velocity, acceleration);
136
137 // limit the speed to the maximum speed
138 if (speed > maxSpeed)
139 {
140 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
141 }
142
143 // move the enemy
144 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
145 }
146
147 if (waypointPassedCount)
148 {
149 (*waypointPassedCount) = passedCount;
150 }
151
152 return position;
153 }
154
155 void EnemyDraw()
156 {
157 for (int i = 0; i < enemyCount; i++)
158 {
159 Enemy enemy = enemies[i];
160 if (enemy.enemyType == ENEMY_TYPE_NONE)
161 {
162 continue;
163 }
164
165 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
166
167 // don't draw any trails for now; might replace this with footprints later
168 // if (enemy.movePathCount > 0)
169 // {
170 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
171 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
172 // }
173 // for (int j = 1; j < enemy.movePathCount; j++)
174 // {
175 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
176 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
177 // DrawLine3D(p, q, GREEN);
178 // }
179
180 switch (enemy.enemyType)
181 {
182 case ENEMY_TYPE_MINION:
183 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
184 enemy.walkedDistance, 0, 0);
185 break;
186 }
187 }
188 }
189
190 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
191 {
192 // damage the tower
193 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
194 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
195 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
196 float explosionRange2 = explosionRange * explosionRange;
197 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
198 // explode the enemy
199 if (tower->damage >= TowerGetMaxHealth(tower))
200 {
201 tower->towerType = TOWER_TYPE_NONE;
202 }
203
204 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
205 explosionSource,
206 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
207
208 enemy->enemyType = ENEMY_TYPE_NONE;
209
210 // push back enemies & dealing damage
211 for (int i = 0; i < enemyCount; i++)
212 {
213 Enemy *other = &enemies[i];
214 if (other->enemyType == ENEMY_TYPE_NONE)
215 {
216 continue;
217 }
218 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
219 if (distanceSqr > 0 && distanceSqr < explosionRange2)
220 {
221 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
222 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
223 EnemyAddDamage(other, explosionDamge);
224 }
225 }
226 }
227
228 void EnemyUpdate()
229 {
230 const float castleX = 0;
231 const float castleY = 0;
232 const float maxPathDistance2 = 0.25f * 0.25f;
233
234 for (int i = 0; i < enemyCount; i++)
235 {
236 Enemy *enemy = &enemies[i];
237 if (enemy->enemyType == ENEMY_TYPE_NONE)
238 {
239 continue;
240 }
241
242 int waypointPassedCount = 0;
243 Vector2 prevPosition = enemy->simPosition;
244 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
245 enemy->startMovingTime = gameTime.time;
246 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
247 // track path of unit
248 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
249 {
250 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
251 {
252 enemy->movePath[j] = enemy->movePath[j - 1];
253 }
254 enemy->movePath[0] = enemy->simPosition;
255 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
256 {
257 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
258 }
259 }
260
261 if (waypointPassedCount > 0)
262 {
263 enemy->currentX = enemy->nextX;
264 enemy->currentY = enemy->nextY;
265 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
266 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
267 {
268 // enemy reached the castle; remove it
269 enemy->enemyType = ENEMY_TYPE_NONE;
270 continue;
271 }
272 }
273 }
274
275 // handle collisions between enemies
276 for (int i = 0; i < enemyCount - 1; i++)
277 {
278 Enemy *enemyA = &enemies[i];
279 if (enemyA->enemyType == ENEMY_TYPE_NONE)
280 {
281 continue;
282 }
283 for (int j = i + 1; j < enemyCount; j++)
284 {
285 Enemy *enemyB = &enemies[j];
286 if (enemyB->enemyType == ENEMY_TYPE_NONE)
287 {
288 continue;
289 }
290 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
291 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
292 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
293 float radiusSum = radiusA + radiusB;
294 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
295 {
296 // collision
297 float distance = sqrtf(distanceSqr);
298 float overlap = radiusSum - distance;
299 // move the enemies apart, but softly; if we have a clog of enemies,
300 // moving them perfectly apart can cause them to jitter
301 float positionCorrection = overlap / 5.0f;
302 Vector2 direction = (Vector2){
303 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
304 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
305 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
306 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
307 }
308 }
309 }
310
311 // handle collisions between enemies and towers
312 for (int i = 0; i < enemyCount; i++)
313 {
314 Enemy *enemy = &enemies[i];
315 if (enemy->enemyType == ENEMY_TYPE_NONE)
316 {
317 continue;
318 }
319 enemy->contactTime -= gameTime.deltaTime;
320 if (enemy->contactTime < 0.0f)
321 {
322 enemy->contactTime = 0.0f;
323 }
324
325 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
326 // linear search over towers; could be optimized by using path finding tower map,
327 // but for now, we keep it simple
328 for (int j = 0; j < towerCount; j++)
329 {
330 Tower *tower = &towers[j];
331 if (tower->towerType == TOWER_TYPE_NONE)
332 {
333 continue;
334 }
335 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
336 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
337 if (distanceSqr > combinedRadius * combinedRadius)
338 {
339 continue;
340 }
341 // potential collision; square / circle intersection
342 float dx = tower->x - enemy->simPosition.x;
343 float dy = tower->y - enemy->simPosition.y;
344 float absDx = fabsf(dx);
345 float absDy = fabsf(dy);
346 Vector3 contactPoint = {0};
347 if (absDx <= 0.5f && absDx <= absDy) {
348 // vertical collision; push the enemy out horizontally
349 float overlap = enemyRadius + 0.5f - absDy;
350 if (overlap < 0.0f)
351 {
352 continue;
353 }
354 float direction = dy > 0.0f ? -1.0f : 1.0f;
355 enemy->simPosition.y += direction * overlap;
356 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
357 }
358 else if (absDy <= 0.5f && absDy <= absDx)
359 {
360 // horizontal collision; push the enemy out vertically
361 float overlap = enemyRadius + 0.5f - absDx;
362 if (overlap < 0.0f)
363 {
364 continue;
365 }
366 float direction = dx > 0.0f ? -1.0f : 1.0f;
367 enemy->simPosition.x += direction * overlap;
368 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
369 }
370 else
371 {
372 // possible collision with a corner
373 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
374 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
375 float cornerX = tower->x + cornerDX;
376 float cornerY = tower->y + cornerDY;
377 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
378 if (cornerDistanceSqr > enemyRadius * enemyRadius)
379 {
380 continue;
381 }
382 // push the enemy out along the diagonal
383 float cornerDistance = sqrtf(cornerDistanceSqr);
384 float overlap = enemyRadius - cornerDistance;
385 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
386 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
387 enemy->simPosition.x -= directionX * overlap;
388 enemy->simPosition.y -= directionY * overlap;
389 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
390 }
391
392 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
393 {
394 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
395 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
396 {
397 EnemyTriggerExplode(enemy, tower, contactPoint);
398 }
399 }
400 }
401 }
402 }
403
404 EnemyId EnemyGetId(Enemy *enemy)
405 {
406 return (EnemyId){enemy - enemies, enemy->generation};
407 }
408
409 Enemy *EnemyTryResolve(EnemyId enemyId)
410 {
411 if (enemyId.index >= ENEMY_MAX_COUNT)
412 {
413 return 0;
414 }
415 Enemy *enemy = &enemies[enemyId.index];
416 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
417 {
418 return 0;
419 }
420 return enemy;
421 }
422
423 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
424 {
425 Enemy *spawn = 0;
426 for (int i = 0; i < enemyCount; i++)
427 {
428 Enemy *enemy = &enemies[i];
429 if (enemy->enemyType == ENEMY_TYPE_NONE)
430 {
431 spawn = enemy;
432 break;
433 }
434 }
435
436 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
437 {
438 spawn = &enemies[enemyCount++];
439 }
440
441 if (spawn)
442 {
443 spawn->currentX = currentX;
444 spawn->currentY = currentY;
445 spawn->nextX = currentX;
446 spawn->nextY = currentY;
447 spawn->simPosition = (Vector2){currentX, currentY};
448 spawn->simVelocity = (Vector2){0, 0};
449 spawn->enemyType = enemyType;
450 spawn->startMovingTime = gameTime.time;
451 spawn->damage = 0.0f;
452 spawn->futureDamage = 0.0f;
453 spawn->generation++;
454 spawn->movePathCount = 0;
455 spawn->walkedDistance = 0.0f;
456 }
457
458 return spawn;
459 }
460
461 int EnemyAddDamageRange(Vector2 position, float range, float damage)
462 {
463 int count = 0;
464 float range2 = range * range;
465 for (int i = 0; i < enemyCount; i++)
466 {
467 Enemy *enemy = &enemies[i];
468 if (enemy->enemyType == ENEMY_TYPE_NONE)
469 {
470 continue;
471 }
472 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
473 if (distance2 <= range2)
474 {
475 EnemyAddDamage(enemy, damage);
476 count++;
477 }
478 }
479 return count;
480 }
481
482 int EnemyAddDamage(Enemy *enemy, float damage)
483 {
484 enemy->damage += damage;
485 if (enemy->damage >= EnemyGetMaxHealth(enemy))
486 {
487 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
488 enemy->enemyType = ENEMY_TYPE_NONE;
489 return 1;
490 }
491
492 return 0;
493 }
494
495 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
496 {
497 int16_t castleX = 0;
498 int16_t castleY = 0;
499 Enemy* closest = 0;
500 int16_t closestDistance = 0;
501 float range2 = range * range;
502 for (int i = 0; i < enemyCount; i++)
503 {
504 Enemy* enemy = &enemies[i];
505 if (enemy->enemyType == ENEMY_TYPE_NONE)
506 {
507 continue;
508 }
509 float maxHealth = EnemyGetMaxHealth(enemy);
510 if (enemy->futureDamage >= maxHealth)
511 {
512 // ignore enemies that will die soon
513 continue;
514 }
515 int16_t dx = castleX - enemy->currentX;
516 int16_t dy = castleY - enemy->currentY;
517 int16_t distance = abs(dx) + abs(dy);
518 if (!closest || distance < closestDistance)
519 {
520 float tdx = towerX - enemy->currentX;
521 float tdy = towerY - enemy->currentY;
522 float tdistance2 = tdx * tdx + tdy * tdy;
523 if (tdistance2 <= range2)
524 {
525 closest = enemy;
526 closestDistance = distance;
527 }
528 }
529 }
530 return closest;
531 }
532
533 int EnemyCount()
534 {
535 int count = 0;
536 for (int i = 0; i < enemyCount; i++)
537 {
538 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
539 {
540 count++;
541 }
542 }
543 return count;
544 }
545
546 void EnemyDrawHealthbars(Camera3D camera)
547 {
548 for (int i = 0; i < enemyCount; i++)
549 {
550 Enemy *enemy = &enemies[i];
551 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
552 {
553 continue;
554 }
555 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
556 float maxHealth = EnemyGetMaxHealth(enemy);
557 float health = maxHealth - enemy->damage;
558 float healthRatio = health / maxHealth;
559
560 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
561 }
562 }
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
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
Just adding the weapon to the minion to make it more unique reveals two rendering issues:
The weapon disappears behind another enemy sprite: This happens because the sprite is drawn in whatever order the enemies are in the list and the sprites draw to the z-buffer. When now another sprite is rendered behind, it will be drawn over the weapon.
There are multiple ways to fix this:
- Draw the sprites back to front: This way, they will be drawn in the correct order, never blocking each other.
- Use depth testing: The shader can discard fragments that are invisible due to the alpha value. Normally, the entire quad is drawn into the z-buffer, but the shader can discard the fragments that are invisible, so the z-buffer will not be updated for these fragments. For blurred sprites, this does not work so well.
- Just ignore the problem and draw without depth writing: We can just draw the sprites without writing to the z-buffer. This way, the sprites will be drawn in the order they are in the list, but the z-buffer will not be updated. This is the easiest way to fix the problem, but it can cause other issues, like sprites that should be behind another sprite being drawn in front.
Since we focus here on simplicity, I will first try the last option and see if it works. Sorting the sprites would be my second choice, but that would require a few more changes in the code.
The second problem is that there are lines on the edges of the sprites. This is due to the texture filtering and is called "bleeding" (see here).
Here is a picture of the sprite sheet and how the sprites are drawn:
The source rectangles from our atlas texture have not enough space between the sprites (padding), so the texture filtering is interpolating the colors of the adjacent pixels belonging to other sprites. Texture bleeding can be observed in may different situations. Light mapping is often suffers from the same problem.
To solve this probleme here, we only have to add a small margin to the sprites in the atlas texture. In this specific case, I can simply select a smaller rectangle from the sprite, since the sprites we use here are not using the entire space.
But in the future when we're adding new sprites, we have to keep this in mind.
Since we are not using mipmaps, a distance of 1 pixel is enough. As always, the topic can be quite complex; there are various different solutions to this problem each with different trade-offs. Luckily, we can ignore this for now.
In case you don't know what mipmaps are, you can read it up here.
Anyway, let's try out how the enemies look without depth writing and with a margin around the sprites.
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
21 #define ENEMY_TYPE_MINION 1
22 #define ENEMY_TYPE_RUNNER 2
23 #define ENEMY_TYPE_SHIELD 3
24 #define ENEMY_TYPE_BOSS 3
25
26 #define PARTICLE_MAX_COUNT 400
27 #define PARTICLE_TYPE_NONE 0
28 #define PARTICLE_TYPE_EXPLOSION 1
29
30 typedef struct Particle
31 {
32 uint8_t particleType;
33 float spawnTime;
34 float lifetime;
35 Vector3 position;
36 Vector3 velocity;
37 Vector3 scale;
38 } Particle;
39
40 #define TOWER_MAX_COUNT 400
41 enum TowerType
42 {
43 TOWER_TYPE_NONE,
44 TOWER_TYPE_BASE,
45 TOWER_TYPE_ARCHER,
46 TOWER_TYPE_BALLISTA,
47 TOWER_TYPE_CATAPULT,
48 TOWER_TYPE_WALL,
49 TOWER_TYPE_COUNT
50 };
51
52 typedef struct HitEffectConfig
53 {
54 float damage;
55 float areaDamageRadius;
56 float pushbackPowerDistance;
57 } HitEffectConfig;
58
59 typedef struct TowerTypeConfig
60 {
61 float cooldown;
62 float range;
63 float projectileSpeed;
64
65 uint8_t cost;
66 uint8_t projectileType;
67 uint16_t maxHealth;
68
69 HitEffectConfig hitEffect;
70 } TowerTypeConfig;
71
72 typedef struct Tower
73 {
74 int16_t x, y;
75 uint8_t towerType;
76 Vector2 lastTargetPosition;
77 float cooldown;
78 float damage;
79 } Tower;
80
81 typedef struct GameTime
82 {
83 float time;
84 float deltaTime;
85 uint32_t frameCount;
86
87 float fixedDeltaTime;
88 // leaving the fixed time stepping to the update functions,
89 // we need to know the fixed time at the start of the frame
90 float fixedTimeStart;
91 // and the number of fixed steps that we have to make this frame
92 // The fixedTime is fixedTimeStart + n * fixedStepCount
93 uint8_t fixedStepCount;
94 } GameTime;
95
96 typedef struct ButtonState {
97 char isSelected;
98 char isDisabled;
99 } ButtonState;
100
101 typedef struct GUIState {
102 int isBlocked;
103 } GUIState;
104
105 typedef enum LevelState
106 {
107 LEVEL_STATE_NONE,
108 LEVEL_STATE_BUILDING,
109 LEVEL_STATE_BUILDING_PLACEMENT,
110 LEVEL_STATE_BATTLE,
111 LEVEL_STATE_WON_WAVE,
112 LEVEL_STATE_LOST_WAVE,
113 LEVEL_STATE_WON_LEVEL,
114 LEVEL_STATE_RESET,
115 } LevelState;
116
117 typedef struct EnemyWave {
118 uint8_t enemyType;
119 uint8_t wave;
120 uint16_t count;
121 float interval;
122 float delay;
123 Vector2 spawnPosition;
124
125 uint16_t spawned;
126 float timeToSpawnNext;
127 } EnemyWave;
128
129 #define ENEMY_MAX_WAVE_COUNT 10
130
131 typedef enum PlacementPhase
132 {
133 PLACEMENT_PHASE_STARTING,
134 PLACEMENT_PHASE_MOVING,
135 PLACEMENT_PHASE_PLACING,
136 } PlacementPhase;
137
138 typedef struct Level
139 {
140 int seed;
141 LevelState state;
142 LevelState nextState;
143 Camera3D camera;
144 int placementMode;
145 PlacementPhase placementPhase;
146 float placementTimer;
147 int16_t placementX;
148 int16_t placementY;
149 Vector2 placementTransitionPosition;
150 PhysicsPoint placementTowerSpring;
151
152 int initialGold;
153 int playerGold;
154
155 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
156 int currentWave;
157 float waveEndTimer;
158 } Level;
159
160 typedef struct DeltaSrc
161 {
162 char x, y;
163 } DeltaSrc;
164
165 typedef struct PathfindingMap
166 {
167 int width, height;
168 float scale;
169 float *distances;
170 long *towerIndex;
171 DeltaSrc *deltaSrc;
172 float maxDistance;
173 Matrix toMapSpace;
174 Matrix toWorldSpace;
175 } PathfindingMap;
176
177 // when we execute the pathfinding algorithm, we need to store the active nodes
178 // in a queue. Each node has a position, a distance from the start, and the
179 // position of the node that we came from.
180 typedef struct PathfindingNode
181 {
182 int16_t x, y, fromX, fromY;
183 float distance;
184 } PathfindingNode;
185
186 typedef struct EnemyId
187 {
188 uint16_t index;
189 uint16_t generation;
190 } EnemyId;
191
192 typedef struct EnemyClassConfig
193 {
194 float speed;
195 float health;
196 float radius;
197 float maxAcceleration;
198 float requiredContactTime;
199 float explosionDamage;
200 float explosionRange;
201 float explosionPushbackPower;
202 int goldValue;
203 } EnemyClassConfig;
204
205 typedef struct Enemy
206 {
207 int16_t currentX, currentY;
208 int16_t nextX, nextY;
209 Vector2 simPosition;
210 Vector2 simVelocity;
211 uint16_t generation;
212 float walkedDistance;
213 float startMovingTime;
214 float damage, futureDamage;
215 float contactTime;
216 uint8_t enemyType;
217 uint8_t movePathCount;
218 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
219 } Enemy;
220
221 // a unit that uses sprites to be drawn
222 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
223 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
224 typedef struct SpriteUnit
225 {
226 Rectangle srcRect;
227 Vector2 offset;
228 int frameCount;
229 float frameDuration;
230 Rectangle srcWeaponIdleRect;
231 Vector2 srcWeaponIdleOffset;
232 Rectangle srcWeaponCooldownRect;
233 Vector2 srcWeaponCooldownOffset;
234 } SpriteUnit;
235
236 #define PROJECTILE_MAX_COUNT 1200
237 #define PROJECTILE_TYPE_NONE 0
238 #define PROJECTILE_TYPE_ARROW 1
239 #define PROJECTILE_TYPE_CATAPULT 2
240 #define PROJECTILE_TYPE_BALLISTA 3
241
242 typedef struct Projectile
243 {
244 uint8_t projectileType;
245 float shootTime;
246 float arrivalTime;
247 float distance;
248 Vector3 position;
249 Vector3 target;
250 Vector3 directionNormal;
251 EnemyId targetEnemy;
252 HitEffectConfig hitEffectConfig;
253 } Projectile;
254
255 //# Function declarations
256 float TowerGetMaxHealth(Tower *tower);
257 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
258 int EnemyAddDamageRange(Vector2 position, float range, float damage);
259 int EnemyAddDamage(Enemy *enemy, float damage);
260
261 //# Enemy functions
262 void EnemyInit();
263 void EnemyDraw();
264 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
265 void EnemyUpdate();
266 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
267 float EnemyGetMaxHealth(Enemy *enemy);
268 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
269 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
270 EnemyId EnemyGetId(Enemy *enemy);
271 Enemy *EnemyTryResolve(EnemyId enemyId);
272 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
273 int EnemyAddDamage(Enemy *enemy, float damage);
274 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
275 int EnemyCount();
276 void EnemyDrawHealthbars(Camera3D camera);
277
278 //# Tower functions
279 void TowerInit();
280 Tower *TowerGetAt(int16_t x, int16_t y);
281 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
282 Tower *GetTowerByType(uint8_t towerType);
283 int GetTowerCosts(uint8_t towerType);
284 float TowerGetMaxHealth(Tower *tower);
285 void TowerDraw();
286 void TowerDrawSingle(Tower tower);
287 void TowerUpdate();
288 void TowerDrawHealthBars(Camera3D camera);
289 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
290
291 //# Particles
292 void ParticleInit();
293 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
294 void ParticleUpdate();
295 void ParticleDraw();
296
297 //# Projectiles
298 void ProjectileInit();
299 void ProjectileDraw();
300 void ProjectileUpdate();
301 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
302
303 //# Pathfinding map
304 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
305 float PathFindingGetDistance(int mapX, int mapY);
306 Vector2 PathFindingGetGradient(Vector3 world);
307 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
308 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
309 void PathFindingMapDraw();
310
311 //# UI
312 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
313
314 //# Level
315 void DrawLevelGround(Level *level);
316 void DrawEnemyPath(Level *level, Color arrowColor);
317
318 //# variables
319 extern Level *currentLevel;
320 extern Enemy enemies[ENEMY_MAX_COUNT];
321 extern int enemyCount;
322 extern EnemyClassConfig enemyClassConfigs[];
323
324 extern GUIState guiState;
325 extern GameTime gameTime;
326 extern Tower towers[TOWER_MAX_COUNT];
327 extern int towerCount;
328
329 extern Texture2D palette, spriteSheet;
330
331 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 };
31
32 Enemy enemies[ENEMY_MAX_COUNT];
33 int enemyCount = 0;
34
35 SpriteUnit enemySprites[] = {
36 [ENEMY_TYPE_MINION] = {
37 .srcRect = {0, 17, 16, 15},
38 .offset = {8.0f, 0.0f},
39 .frameCount = 6,
40 .frameDuration = 0.1f,
41 .srcWeaponIdleRect = {1, 33, 14, 14},
42 .srcWeaponIdleOffset = {7.0f, 0.0f},
43 },
44 [ENEMY_TYPE_RUNNER] = {
45 .srcRect = {0, 16, 16, 16},
46 .offset = {8.0f, 0.0f},
47 .frameCount = 6,
48 .frameDuration = 0.1f,
49 },
50 };
51
52 void EnemyInit()
53 {
54 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
55 {
56 enemies[i] = (Enemy){0};
57 }
58 enemyCount = 0;
59 }
60
61 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
62 {
63 return enemyClassConfigs[enemy->enemyType].speed;
64 }
65
66 float EnemyGetMaxHealth(Enemy *enemy)
67 {
68 return enemyClassConfigs[enemy->enemyType].health;
69 }
70
71 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
72 {
73 int16_t castleX = 0;
74 int16_t castleY = 0;
75 int16_t dx = castleX - currentX;
76 int16_t dy = castleY - currentY;
77 if (dx == 0 && dy == 0)
78 {
79 *nextX = currentX;
80 *nextY = currentY;
81 return 1;
82 }
83 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
84
85 if (gradient.x == 0 && gradient.y == 0)
86 {
87 *nextX = currentX;
88 *nextY = currentY;
89 return 1;
90 }
91
92 if (fabsf(gradient.x) > fabsf(gradient.y))
93 {
94 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
95 *nextY = currentY;
96 return 0;
97 }
98 *nextX = currentX;
99 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
100 return 0;
101 }
102
103
104 // this function predicts the movement of the unit for the next deltaT seconds
105 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
106 {
107 const float pointReachedDistance = 0.25f;
108 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
109 const float maxSimStepTime = 0.015625f;
110
111 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
112 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
113 int16_t nextX = enemy->nextX;
114 int16_t nextY = enemy->nextY;
115 Vector2 position = enemy->simPosition;
116 int passedCount = 0;
117 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
118 {
119 float stepTime = fminf(deltaT - t, maxSimStepTime);
120 Vector2 target = (Vector2){nextX, nextY};
121 float speed = Vector2Length(*velocity);
122 // draw the target position for debugging
123 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
124 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
125 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
126 {
127 // we reached the target position, let's move to the next waypoint
128 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
129 target = (Vector2){nextX, nextY};
130 // track how many waypoints we passed
131 passedCount++;
132 }
133
134 // acceleration towards the target
135 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
136 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
137 *velocity = Vector2Add(*velocity, acceleration);
138
139 // limit the speed to the maximum speed
140 if (speed > maxSpeed)
141 {
142 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
143 }
144
145 // move the enemy
146 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
147 }
148
149 if (waypointPassedCount)
150 {
151 (*waypointPassedCount) = passedCount;
152 }
153
154 return position;
155 }
156
157 void EnemyDraw()
158 {
159 rlDrawRenderBatchActive();
160 rlDisableDepthMask();
161 for (int i = 0; i < enemyCount; i++)
162 {
163 Enemy enemy = enemies[i];
164 if (enemy.enemyType == ENEMY_TYPE_NONE)
165 {
166 continue;
167 }
168
169 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
170
171 // don't draw any trails for now; might replace this with footprints later
172 // if (enemy.movePathCount > 0)
173 // {
174 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
175 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
176 // }
177 // for (int j = 1; j < enemy.movePathCount; j++)
178 // {
179 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
180 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
181 // DrawLine3D(p, q, GREEN);
182 // }
183
184 switch (enemy.enemyType)
185 {
186 case ENEMY_TYPE_MINION:
187 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
188 enemy.walkedDistance, 0, 0);
189 break;
190 }
191 }
192 rlDrawRenderBatchActive();
193 rlEnableDepthMask();
194 }
195
196 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
197 {
198 // damage the tower
199 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
200 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
201 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
202 float explosionRange2 = explosionRange * explosionRange;
203 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
204 // explode the enemy
205 if (tower->damage >= TowerGetMaxHealth(tower))
206 {
207 tower->towerType = TOWER_TYPE_NONE;
208 }
209
210 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
211 explosionSource,
212 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
213
214 enemy->enemyType = ENEMY_TYPE_NONE;
215
216 // push back enemies & dealing damage
217 for (int i = 0; i < enemyCount; i++)
218 {
219 Enemy *other = &enemies[i];
220 if (other->enemyType == ENEMY_TYPE_NONE)
221 {
222 continue;
223 }
224 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
225 if (distanceSqr > 0 && distanceSqr < explosionRange2)
226 {
227 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
228 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
229 EnemyAddDamage(other, explosionDamge);
230 }
231 }
232 }
233
234 void EnemyUpdate()
235 {
236 const float castleX = 0;
237 const float castleY = 0;
238 const float maxPathDistance2 = 0.25f * 0.25f;
239
240 for (int i = 0; i < enemyCount; i++)
241 {
242 Enemy *enemy = &enemies[i];
243 if (enemy->enemyType == ENEMY_TYPE_NONE)
244 {
245 continue;
246 }
247
248 int waypointPassedCount = 0;
249 Vector2 prevPosition = enemy->simPosition;
250 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
251 enemy->startMovingTime = gameTime.time;
252 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
253 // track path of unit
254 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
255 {
256 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
257 {
258 enemy->movePath[j] = enemy->movePath[j - 1];
259 }
260 enemy->movePath[0] = enemy->simPosition;
261 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
262 {
263 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
264 }
265 }
266
267 if (waypointPassedCount > 0)
268 {
269 enemy->currentX = enemy->nextX;
270 enemy->currentY = enemy->nextY;
271 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
272 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
273 {
274 // enemy reached the castle; remove it
275 enemy->enemyType = ENEMY_TYPE_NONE;
276 continue;
277 }
278 }
279 }
280
281 // handle collisions between enemies
282 for (int i = 0; i < enemyCount - 1; i++)
283 {
284 Enemy *enemyA = &enemies[i];
285 if (enemyA->enemyType == ENEMY_TYPE_NONE)
286 {
287 continue;
288 }
289 for (int j = i + 1; j < enemyCount; j++)
290 {
291 Enemy *enemyB = &enemies[j];
292 if (enemyB->enemyType == ENEMY_TYPE_NONE)
293 {
294 continue;
295 }
296 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
297 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
298 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
299 float radiusSum = radiusA + radiusB;
300 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
301 {
302 // collision
303 float distance = sqrtf(distanceSqr);
304 float overlap = radiusSum - distance;
305 // move the enemies apart, but softly; if we have a clog of enemies,
306 // moving them perfectly apart can cause them to jitter
307 float positionCorrection = overlap / 5.0f;
308 Vector2 direction = (Vector2){
309 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
310 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
311 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
312 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
313 }
314 }
315 }
316
317 // handle collisions between enemies and towers
318 for (int i = 0; i < enemyCount; i++)
319 {
320 Enemy *enemy = &enemies[i];
321 if (enemy->enemyType == ENEMY_TYPE_NONE)
322 {
323 continue;
324 }
325 enemy->contactTime -= gameTime.deltaTime;
326 if (enemy->contactTime < 0.0f)
327 {
328 enemy->contactTime = 0.0f;
329 }
330
331 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
332 // linear search over towers; could be optimized by using path finding tower map,
333 // but for now, we keep it simple
334 for (int j = 0; j < towerCount; j++)
335 {
336 Tower *tower = &towers[j];
337 if (tower->towerType == TOWER_TYPE_NONE)
338 {
339 continue;
340 }
341 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
342 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
343 if (distanceSqr > combinedRadius * combinedRadius)
344 {
345 continue;
346 }
347 // potential collision; square / circle intersection
348 float dx = tower->x - enemy->simPosition.x;
349 float dy = tower->y - enemy->simPosition.y;
350 float absDx = fabsf(dx);
351 float absDy = fabsf(dy);
352 Vector3 contactPoint = {0};
353 if (absDx <= 0.5f && absDx <= absDy) {
354 // vertical collision; push the enemy out horizontally
355 float overlap = enemyRadius + 0.5f - absDy;
356 if (overlap < 0.0f)
357 {
358 continue;
359 }
360 float direction = dy > 0.0f ? -1.0f : 1.0f;
361 enemy->simPosition.y += direction * overlap;
362 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
363 }
364 else if (absDy <= 0.5f && absDy <= absDx)
365 {
366 // horizontal collision; push the enemy out vertically
367 float overlap = enemyRadius + 0.5f - absDx;
368 if (overlap < 0.0f)
369 {
370 continue;
371 }
372 float direction = dx > 0.0f ? -1.0f : 1.0f;
373 enemy->simPosition.x += direction * overlap;
374 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
375 }
376 else
377 {
378 // possible collision with a corner
379 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
380 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
381 float cornerX = tower->x + cornerDX;
382 float cornerY = tower->y + cornerDY;
383 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
384 if (cornerDistanceSqr > enemyRadius * enemyRadius)
385 {
386 continue;
387 }
388 // push the enemy out along the diagonal
389 float cornerDistance = sqrtf(cornerDistanceSqr);
390 float overlap = enemyRadius - cornerDistance;
391 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
392 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
393 enemy->simPosition.x -= directionX * overlap;
394 enemy->simPosition.y -= directionY * overlap;
395 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
396 }
397
398 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
399 {
400 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
401 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
402 {
403 EnemyTriggerExplode(enemy, tower, contactPoint);
404 }
405 }
406 }
407 }
408 }
409
410 EnemyId EnemyGetId(Enemy *enemy)
411 {
412 return (EnemyId){enemy - enemies, enemy->generation};
413 }
414
415 Enemy *EnemyTryResolve(EnemyId enemyId)
416 {
417 if (enemyId.index >= ENEMY_MAX_COUNT)
418 {
419 return 0;
420 }
421 Enemy *enemy = &enemies[enemyId.index];
422 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
423 {
424 return 0;
425 }
426 return enemy;
427 }
428
429 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
430 {
431 Enemy *spawn = 0;
432 for (int i = 0; i < enemyCount; i++)
433 {
434 Enemy *enemy = &enemies[i];
435 if (enemy->enemyType == ENEMY_TYPE_NONE)
436 {
437 spawn = enemy;
438 break;
439 }
440 }
441
442 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
443 {
444 spawn = &enemies[enemyCount++];
445 }
446
447 if (spawn)
448 {
449 spawn->currentX = currentX;
450 spawn->currentY = currentY;
451 spawn->nextX = currentX;
452 spawn->nextY = currentY;
453 spawn->simPosition = (Vector2){currentX, currentY};
454 spawn->simVelocity = (Vector2){0, 0};
455 spawn->enemyType = enemyType;
456 spawn->startMovingTime = gameTime.time;
457 spawn->damage = 0.0f;
458 spawn->futureDamage = 0.0f;
459 spawn->generation++;
460 spawn->movePathCount = 0;
461 spawn->walkedDistance = 0.0f;
462 }
463
464 return spawn;
465 }
466
467 int EnemyAddDamageRange(Vector2 position, float range, float damage)
468 {
469 int count = 0;
470 float range2 = range * range;
471 for (int i = 0; i < enemyCount; i++)
472 {
473 Enemy *enemy = &enemies[i];
474 if (enemy->enemyType == ENEMY_TYPE_NONE)
475 {
476 continue;
477 }
478 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
479 if (distance2 <= range2)
480 {
481 EnemyAddDamage(enemy, damage);
482 count++;
483 }
484 }
485 return count;
486 }
487
488 int EnemyAddDamage(Enemy *enemy, float damage)
489 {
490 enemy->damage += damage;
491 if (enemy->damage >= EnemyGetMaxHealth(enemy))
492 {
493 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
494 enemy->enemyType = ENEMY_TYPE_NONE;
495 return 1;
496 }
497
498 return 0;
499 }
500
501 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
502 {
503 int16_t castleX = 0;
504 int16_t castleY = 0;
505 Enemy* closest = 0;
506 int16_t closestDistance = 0;
507 float range2 = range * range;
508 for (int i = 0; i < enemyCount; i++)
509 {
510 Enemy* enemy = &enemies[i];
511 if (enemy->enemyType == ENEMY_TYPE_NONE)
512 {
513 continue;
514 }
515 float maxHealth = EnemyGetMaxHealth(enemy);
516 if (enemy->futureDamage >= maxHealth)
517 {
518 // ignore enemies that will die soon
519 continue;
520 }
521 int16_t dx = castleX - enemy->currentX;
522 int16_t dy = castleY - enemy->currentY;
523 int16_t distance = abs(dx) + abs(dy);
524 if (!closest || distance < closestDistance)
525 {
526 float tdx = towerX - enemy->currentX;
527 float tdy = towerY - enemy->currentY;
528 float tdistance2 = tdx * tdx + tdy * tdy;
529 if (tdistance2 <= range2)
530 {
531 closest = enemy;
532 closestDistance = distance;
533 }
534 }
535 }
536 return closest;
537 }
538
539 int EnemyCount()
540 {
541 int count = 0;
542 for (int i = 0; i < enemyCount; i++)
543 {
544 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
545 {
546 count++;
547 }
548 }
549 return count;
550 }
551
552 void EnemyDrawHealthbars(Camera3D camera)
553 {
554 for (int i = 0; i < enemyCount; i++)
555 {
556 Enemy *enemy = &enemies[i];
557 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
558 {
559 continue;
560 }
561 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
562 float maxHealth = EnemyGetMaxHealth(enemy);
563 float health = maxHealth - enemy->damage;
564 float healthRatio = health / maxHealth;
565
566 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
567 }
568 }
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
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
Zooming in, we can see the difference:
The bleeding is gone and the render order looks good - moving the sword from the left to the right hand helps to reduce the overlapping. Since the enemies have a radius and push each other away, overlapping is therefore only happen in rare cases, so this should be good enough for now.
One important bit: When changing the rendering state (calling rlDisableDepthMask()), we have to first flush the batch to make sure the sprites are drawn in the correct order. This is what the rlDrawRenderBatchActive(); call does. If we don't do this, the render state change may not be applied until the batch is flushed (raylib is internally batching the draw calls).
One more missing thing is, that the weapon has also an animation, so let's add that as well.
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
21 #define ENEMY_TYPE_MINION 1
22 #define ENEMY_TYPE_RUNNER 2
23 #define ENEMY_TYPE_SHIELD 3
24 #define ENEMY_TYPE_BOSS 3
25
26 #define PARTICLE_MAX_COUNT 400
27 #define PARTICLE_TYPE_NONE 0
28 #define PARTICLE_TYPE_EXPLOSION 1
29
30 typedef struct Particle
31 {
32 uint8_t particleType;
33 float spawnTime;
34 float lifetime;
35 Vector3 position;
36 Vector3 velocity;
37 Vector3 scale;
38 } Particle;
39
40 #define TOWER_MAX_COUNT 400
41 enum TowerType
42 {
43 TOWER_TYPE_NONE,
44 TOWER_TYPE_BASE,
45 TOWER_TYPE_ARCHER,
46 TOWER_TYPE_BALLISTA,
47 TOWER_TYPE_CATAPULT,
48 TOWER_TYPE_WALL,
49 TOWER_TYPE_COUNT
50 };
51
52 typedef struct HitEffectConfig
53 {
54 float damage;
55 float areaDamageRadius;
56 float pushbackPowerDistance;
57 } HitEffectConfig;
58
59 typedef struct TowerTypeConfig
60 {
61 float cooldown;
62 float range;
63 float projectileSpeed;
64
65 uint8_t cost;
66 uint8_t projectileType;
67 uint16_t maxHealth;
68
69 HitEffectConfig hitEffect;
70 } TowerTypeConfig;
71
72 typedef struct Tower
73 {
74 int16_t x, y;
75 uint8_t towerType;
76 Vector2 lastTargetPosition;
77 float cooldown;
78 float damage;
79 } Tower;
80
81 typedef struct GameTime
82 {
83 float time;
84 float deltaTime;
85 uint32_t frameCount;
86
87 float fixedDeltaTime;
88 // leaving the fixed time stepping to the update functions,
89 // we need to know the fixed time at the start of the frame
90 float fixedTimeStart;
91 // and the number of fixed steps that we have to make this frame
92 // The fixedTime is fixedTimeStart + n * fixedStepCount
93 uint8_t fixedStepCount;
94 } GameTime;
95
96 typedef struct ButtonState {
97 char isSelected;
98 char isDisabled;
99 } ButtonState;
100
101 typedef struct GUIState {
102 int isBlocked;
103 } GUIState;
104
105 typedef enum LevelState
106 {
107 LEVEL_STATE_NONE,
108 LEVEL_STATE_BUILDING,
109 LEVEL_STATE_BUILDING_PLACEMENT,
110 LEVEL_STATE_BATTLE,
111 LEVEL_STATE_WON_WAVE,
112 LEVEL_STATE_LOST_WAVE,
113 LEVEL_STATE_WON_LEVEL,
114 LEVEL_STATE_RESET,
115 } LevelState;
116
117 typedef struct EnemyWave {
118 uint8_t enemyType;
119 uint8_t wave;
120 uint16_t count;
121 float interval;
122 float delay;
123 Vector2 spawnPosition;
124
125 uint16_t spawned;
126 float timeToSpawnNext;
127 } EnemyWave;
128
129 #define ENEMY_MAX_WAVE_COUNT 10
130
131 typedef enum PlacementPhase
132 {
133 PLACEMENT_PHASE_STARTING,
134 PLACEMENT_PHASE_MOVING,
135 PLACEMENT_PHASE_PLACING,
136 } PlacementPhase;
137
138 typedef struct Level
139 {
140 int seed;
141 LevelState state;
142 LevelState nextState;
143 Camera3D camera;
144 int placementMode;
145 PlacementPhase placementPhase;
146 float placementTimer;
147 int16_t placementX;
148 int16_t placementY;
149 Vector2 placementTransitionPosition;
150 PhysicsPoint placementTowerSpring;
151
152 int initialGold;
153 int playerGold;
154
155 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
156 int currentWave;
157 float waveEndTimer;
158 } Level;
159
160 typedef struct DeltaSrc
161 {
162 char x, y;
163 } DeltaSrc;
164
165 typedef struct PathfindingMap
166 {
167 int width, height;
168 float scale;
169 float *distances;
170 long *towerIndex;
171 DeltaSrc *deltaSrc;
172 float maxDistance;
173 Matrix toMapSpace;
174 Matrix toWorldSpace;
175 } PathfindingMap;
176
177 // when we execute the pathfinding algorithm, we need to store the active nodes
178 // in a queue. Each node has a position, a distance from the start, and the
179 // position of the node that we came from.
180 typedef struct PathfindingNode
181 {
182 int16_t x, y, fromX, fromY;
183 float distance;
184 } PathfindingNode;
185
186 typedef struct EnemyId
187 {
188 uint16_t index;
189 uint16_t generation;
190 } EnemyId;
191
192 typedef struct EnemyClassConfig
193 {
194 float speed;
195 float health;
196 float radius;
197 float maxAcceleration;
198 float requiredContactTime;
199 float explosionDamage;
200 float explosionRange;
201 float explosionPushbackPower;
202 int goldValue;
203 } EnemyClassConfig;
204
205 typedef struct Enemy
206 {
207 int16_t currentX, currentY;
208 int16_t nextX, nextY;
209 Vector2 simPosition;
210 Vector2 simVelocity;
211 uint16_t generation;
212 float walkedDistance;
213 float startMovingTime;
214 float damage, futureDamage;
215 float contactTime;
216 uint8_t enemyType;
217 uint8_t movePathCount;
218 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
219 } Enemy;
220
221 // a unit that uses sprites to be drawn
222 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
223 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
224 typedef struct SpriteUnit
225 {
226 Rectangle srcRect;
227 Vector2 offset;
228 int frameCount;
229 float frameDuration;
230 Rectangle srcWeaponIdleRect;
231 Vector2 srcWeaponIdleOffset;
232 int srcWeaponIdleFrameCount;
233 int srcWeaponIdleFrameWidth;
234 float srcWeaponIdleFrameDuration;
235 Rectangle srcWeaponCooldownRect;
236 Vector2 srcWeaponCooldownOffset;
237 } SpriteUnit;
238
239 #define PROJECTILE_MAX_COUNT 1200
240 #define PROJECTILE_TYPE_NONE 0
241 #define PROJECTILE_TYPE_ARROW 1
242 #define PROJECTILE_TYPE_CATAPULT 2
243 #define PROJECTILE_TYPE_BALLISTA 3
244
245 typedef struct Projectile
246 {
247 uint8_t projectileType;
248 float shootTime;
249 float arrivalTime;
250 float distance;
251 Vector3 position;
252 Vector3 target;
253 Vector3 directionNormal;
254 EnemyId targetEnemy;
255 HitEffectConfig hitEffectConfig;
256 } Projectile;
257
258 //# Function declarations
259 float TowerGetMaxHealth(Tower *tower);
260 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
261 int EnemyAddDamageRange(Vector2 position, float range, float damage);
262 int EnemyAddDamage(Enemy *enemy, float damage);
263
264 //# Enemy functions
265 void EnemyInit();
266 void EnemyDraw();
267 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
268 void EnemyUpdate();
269 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
270 float EnemyGetMaxHealth(Enemy *enemy);
271 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
272 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
273 EnemyId EnemyGetId(Enemy *enemy);
274 Enemy *EnemyTryResolve(EnemyId enemyId);
275 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
276 int EnemyAddDamage(Enemy *enemy, float damage);
277 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
278 int EnemyCount();
279 void EnemyDrawHealthbars(Camera3D camera);
280
281 //# Tower functions
282 void TowerInit();
283 Tower *TowerGetAt(int16_t x, int16_t y);
284 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
285 Tower *GetTowerByType(uint8_t towerType);
286 int GetTowerCosts(uint8_t towerType);
287 float TowerGetMaxHealth(Tower *tower);
288 void TowerDraw();
289 void TowerDrawSingle(Tower tower);
290 void TowerUpdate();
291 void TowerDrawHealthBars(Camera3D camera);
292 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
293
294 //# Particles
295 void ParticleInit();
296 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
297 void ParticleUpdate();
298 void ParticleDraw();
299
300 //# Projectiles
301 void ProjectileInit();
302 void ProjectileDraw();
303 void ProjectileUpdate();
304 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
305
306 //# Pathfinding map
307 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
308 float PathFindingGetDistance(int mapX, int mapY);
309 Vector2 PathFindingGetGradient(Vector3 world);
310 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
311 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
312 void PathFindingMapDraw();
313
314 //# UI
315 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
316
317 //# Level
318 void DrawLevelGround(Level *level);
319 void DrawEnemyPath(Level *level, Color arrowColor);
320
321 //# variables
322 extern Level *currentLevel;
323 extern Enemy enemies[ENEMY_MAX_COUNT];
324 extern int enemyCount;
325 extern EnemyClassConfig enemyClassConfigs[];
326
327 extern GUIState guiState;
328 extern GameTime gameTime;
329 extern Tower towers[TOWER_MAX_COUNT];
330 extern int towerCount;
331
332 extern Texture2D palette, spriteSheet;
333
334 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 };
31
32 Enemy enemies[ENEMY_MAX_COUNT];
33 int enemyCount = 0;
34
35 SpriteUnit enemySprites[] = {
36 [ENEMY_TYPE_MINION] = {
37 .srcRect = {0, 17, 16, 15},
38 .offset = {8.0f, 0.0f},
39 .frameCount = 6,
40 .frameDuration = 0.1f,
41 .srcWeaponIdleRect = {1, 33, 15, 14},
42 .srcWeaponIdleFrameCount = 6,
43 .srcWeaponIdleFrameWidth = 16,
44 .srcWeaponIdleFrameDuration = 0.1f,
45 .srcWeaponIdleOffset = {7.0f, 0.0f},
46 },
47 [ENEMY_TYPE_RUNNER] = {
48 .srcRect = {0, 16, 16, 16},
49 .offset = {8.0f, 0.0f},
50 .frameCount = 6,
51 .frameDuration = 0.1f,
52 },
53 };
54
55 void EnemyInit()
56 {
57 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
58 {
59 enemies[i] = (Enemy){0};
60 }
61 enemyCount = 0;
62 }
63
64 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
65 {
66 return enemyClassConfigs[enemy->enemyType].speed;
67 }
68
69 float EnemyGetMaxHealth(Enemy *enemy)
70 {
71 return enemyClassConfigs[enemy->enemyType].health;
72 }
73
74 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
75 {
76 int16_t castleX = 0;
77 int16_t castleY = 0;
78 int16_t dx = castleX - currentX;
79 int16_t dy = castleY - currentY;
80 if (dx == 0 && dy == 0)
81 {
82 *nextX = currentX;
83 *nextY = currentY;
84 return 1;
85 }
86 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
87
88 if (gradient.x == 0 && gradient.y == 0)
89 {
90 *nextX = currentX;
91 *nextY = currentY;
92 return 1;
93 }
94
95 if (fabsf(gradient.x) > fabsf(gradient.y))
96 {
97 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
98 *nextY = currentY;
99 return 0;
100 }
101 *nextX = currentX;
102 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
103 return 0;
104 }
105
106
107 // this function predicts the movement of the unit for the next deltaT seconds
108 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
109 {
110 const float pointReachedDistance = 0.25f;
111 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
112 const float maxSimStepTime = 0.015625f;
113
114 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
115 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
116 int16_t nextX = enemy->nextX;
117 int16_t nextY = enemy->nextY;
118 Vector2 position = enemy->simPosition;
119 int passedCount = 0;
120 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
121 {
122 float stepTime = fminf(deltaT - t, maxSimStepTime);
123 Vector2 target = (Vector2){nextX, nextY};
124 float speed = Vector2Length(*velocity);
125 // draw the target position for debugging
126 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
127 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
128 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
129 {
130 // we reached the target position, let's move to the next waypoint
131 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
132 target = (Vector2){nextX, nextY};
133 // track how many waypoints we passed
134 passedCount++;
135 }
136
137 // acceleration towards the target
138 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
139 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
140 *velocity = Vector2Add(*velocity, acceleration);
141
142 // limit the speed to the maximum speed
143 if (speed > maxSpeed)
144 {
145 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
146 }
147
148 // move the enemy
149 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
150 }
151
152 if (waypointPassedCount)
153 {
154 (*waypointPassedCount) = passedCount;
155 }
156
157 return position;
158 }
159
160 void EnemyDraw()
161 {
162 rlDrawRenderBatchActive();
163 rlDisableDepthMask();
164 for (int i = 0; i < enemyCount; i++)
165 {
166 Enemy enemy = enemies[i];
167 if (enemy.enemyType == ENEMY_TYPE_NONE)
168 {
169 continue;
170 }
171
172 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
173
174 // don't draw any trails for now; might replace this with footprints later
175 // if (enemy.movePathCount > 0)
176 // {
177 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
178 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
179 // }
180 // for (int j = 1; j < enemy.movePathCount; j++)
181 // {
182 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
183 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
184 // DrawLine3D(p, q, GREEN);
185 // }
186
187 switch (enemy.enemyType)
188 {
189 case ENEMY_TYPE_MINION:
190 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
191 enemy.walkedDistance, 0, 0);
192 break;
193 }
194 }
195 rlDrawRenderBatchActive();
196 rlEnableDepthMask();
197 }
198
199 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
200 {
201 // damage the tower
202 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
203 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
204 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
205 float explosionRange2 = explosionRange * explosionRange;
206 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
207 // explode the enemy
208 if (tower->damage >= TowerGetMaxHealth(tower))
209 {
210 tower->towerType = TOWER_TYPE_NONE;
211 }
212
213 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
214 explosionSource,
215 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
216
217 enemy->enemyType = ENEMY_TYPE_NONE;
218
219 // push back enemies & dealing damage
220 for (int i = 0; i < enemyCount; i++)
221 {
222 Enemy *other = &enemies[i];
223 if (other->enemyType == ENEMY_TYPE_NONE)
224 {
225 continue;
226 }
227 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
228 if (distanceSqr > 0 && distanceSqr < explosionRange2)
229 {
230 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
231 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
232 EnemyAddDamage(other, explosionDamge);
233 }
234 }
235 }
236
237 void EnemyUpdate()
238 {
239 const float castleX = 0;
240 const float castleY = 0;
241 const float maxPathDistance2 = 0.25f * 0.25f;
242
243 for (int i = 0; i < enemyCount; i++)
244 {
245 Enemy *enemy = &enemies[i];
246 if (enemy->enemyType == ENEMY_TYPE_NONE)
247 {
248 continue;
249 }
250
251 int waypointPassedCount = 0;
252 Vector2 prevPosition = enemy->simPosition;
253 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
254 enemy->startMovingTime = gameTime.time;
255 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
256 // track path of unit
257 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
258 {
259 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
260 {
261 enemy->movePath[j] = enemy->movePath[j - 1];
262 }
263 enemy->movePath[0] = enemy->simPosition;
264 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
265 {
266 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
267 }
268 }
269
270 if (waypointPassedCount > 0)
271 {
272 enemy->currentX = enemy->nextX;
273 enemy->currentY = enemy->nextY;
274 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
275 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
276 {
277 // enemy reached the castle; remove it
278 enemy->enemyType = ENEMY_TYPE_NONE;
279 continue;
280 }
281 }
282 }
283
284 // handle collisions between enemies
285 for (int i = 0; i < enemyCount - 1; i++)
286 {
287 Enemy *enemyA = &enemies[i];
288 if (enemyA->enemyType == ENEMY_TYPE_NONE)
289 {
290 continue;
291 }
292 for (int j = i + 1; j < enemyCount; j++)
293 {
294 Enemy *enemyB = &enemies[j];
295 if (enemyB->enemyType == ENEMY_TYPE_NONE)
296 {
297 continue;
298 }
299 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
300 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
301 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
302 float radiusSum = radiusA + radiusB;
303 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
304 {
305 // collision
306 float distance = sqrtf(distanceSqr);
307 float overlap = radiusSum - distance;
308 // move the enemies apart, but softly; if we have a clog of enemies,
309 // moving them perfectly apart can cause them to jitter
310 float positionCorrection = overlap / 5.0f;
311 Vector2 direction = (Vector2){
312 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
313 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
314 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
315 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
316 }
317 }
318 }
319
320 // handle collisions between enemies and towers
321 for (int i = 0; i < enemyCount; i++)
322 {
323 Enemy *enemy = &enemies[i];
324 if (enemy->enemyType == ENEMY_TYPE_NONE)
325 {
326 continue;
327 }
328 enemy->contactTime -= gameTime.deltaTime;
329 if (enemy->contactTime < 0.0f)
330 {
331 enemy->contactTime = 0.0f;
332 }
333
334 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
335 // linear search over towers; could be optimized by using path finding tower map,
336 // but for now, we keep it simple
337 for (int j = 0; j < towerCount; j++)
338 {
339 Tower *tower = &towers[j];
340 if (tower->towerType == TOWER_TYPE_NONE)
341 {
342 continue;
343 }
344 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
345 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
346 if (distanceSqr > combinedRadius * combinedRadius)
347 {
348 continue;
349 }
350 // potential collision; square / circle intersection
351 float dx = tower->x - enemy->simPosition.x;
352 float dy = tower->y - enemy->simPosition.y;
353 float absDx = fabsf(dx);
354 float absDy = fabsf(dy);
355 Vector3 contactPoint = {0};
356 if (absDx <= 0.5f && absDx <= absDy) {
357 // vertical collision; push the enemy out horizontally
358 float overlap = enemyRadius + 0.5f - absDy;
359 if (overlap < 0.0f)
360 {
361 continue;
362 }
363 float direction = dy > 0.0f ? -1.0f : 1.0f;
364 enemy->simPosition.y += direction * overlap;
365 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
366 }
367 else if (absDy <= 0.5f && absDy <= absDx)
368 {
369 // horizontal collision; push the enemy out vertically
370 float overlap = enemyRadius + 0.5f - absDx;
371 if (overlap < 0.0f)
372 {
373 continue;
374 }
375 float direction = dx > 0.0f ? -1.0f : 1.0f;
376 enemy->simPosition.x += direction * overlap;
377 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
378 }
379 else
380 {
381 // possible collision with a corner
382 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
383 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
384 float cornerX = tower->x + cornerDX;
385 float cornerY = tower->y + cornerDY;
386 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
387 if (cornerDistanceSqr > enemyRadius * enemyRadius)
388 {
389 continue;
390 }
391 // push the enemy out along the diagonal
392 float cornerDistance = sqrtf(cornerDistanceSqr);
393 float overlap = enemyRadius - cornerDistance;
394 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
395 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
396 enemy->simPosition.x -= directionX * overlap;
397 enemy->simPosition.y -= directionY * overlap;
398 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
399 }
400
401 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
402 {
403 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
404 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
405 {
406 EnemyTriggerExplode(enemy, tower, contactPoint);
407 }
408 }
409 }
410 }
411 }
412
413 EnemyId EnemyGetId(Enemy *enemy)
414 {
415 return (EnemyId){enemy - enemies, enemy->generation};
416 }
417
418 Enemy *EnemyTryResolve(EnemyId enemyId)
419 {
420 if (enemyId.index >= ENEMY_MAX_COUNT)
421 {
422 return 0;
423 }
424 Enemy *enemy = &enemies[enemyId.index];
425 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
426 {
427 return 0;
428 }
429 return enemy;
430 }
431
432 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
433 {
434 Enemy *spawn = 0;
435 for (int i = 0; i < enemyCount; i++)
436 {
437 Enemy *enemy = &enemies[i];
438 if (enemy->enemyType == ENEMY_TYPE_NONE)
439 {
440 spawn = enemy;
441 break;
442 }
443 }
444
445 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
446 {
447 spawn = &enemies[enemyCount++];
448 }
449
450 if (spawn)
451 {
452 spawn->currentX = currentX;
453 spawn->currentY = currentY;
454 spawn->nextX = currentX;
455 spawn->nextY = currentY;
456 spawn->simPosition = (Vector2){currentX, currentY};
457 spawn->simVelocity = (Vector2){0, 0};
458 spawn->enemyType = enemyType;
459 spawn->startMovingTime = gameTime.time;
460 spawn->damage = 0.0f;
461 spawn->futureDamage = 0.0f;
462 spawn->generation++;
463 spawn->movePathCount = 0;
464 spawn->walkedDistance = 0.0f;
465 }
466
467 return spawn;
468 }
469
470 int EnemyAddDamageRange(Vector2 position, float range, float damage)
471 {
472 int count = 0;
473 float range2 = range * range;
474 for (int i = 0; i < enemyCount; i++)
475 {
476 Enemy *enemy = &enemies[i];
477 if (enemy->enemyType == ENEMY_TYPE_NONE)
478 {
479 continue;
480 }
481 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
482 if (distance2 <= range2)
483 {
484 EnemyAddDamage(enemy, damage);
485 count++;
486 }
487 }
488 return count;
489 }
490
491 int EnemyAddDamage(Enemy *enemy, float damage)
492 {
493 enemy->damage += damage;
494 if (enemy->damage >= EnemyGetMaxHealth(enemy))
495 {
496 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
497 enemy->enemyType = ENEMY_TYPE_NONE;
498 return 1;
499 }
500
501 return 0;
502 }
503
504 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
505 {
506 int16_t castleX = 0;
507 int16_t castleY = 0;
508 Enemy* closest = 0;
509 int16_t closestDistance = 0;
510 float range2 = range * range;
511 for (int i = 0; i < enemyCount; i++)
512 {
513 Enemy* enemy = &enemies[i];
514 if (enemy->enemyType == ENEMY_TYPE_NONE)
515 {
516 continue;
517 }
518 float maxHealth = EnemyGetMaxHealth(enemy);
519 if (enemy->futureDamage >= maxHealth)
520 {
521 // ignore enemies that will die soon
522 continue;
523 }
524 int16_t dx = castleX - enemy->currentX;
525 int16_t dy = castleY - enemy->currentY;
526 int16_t distance = abs(dx) + abs(dy);
527 if (!closest || distance < closestDistance)
528 {
529 float tdx = towerX - enemy->currentX;
530 float tdy = towerY - enemy->currentY;
531 float tdistance2 = tdx * tdx + tdy * tdy;
532 if (tdistance2 <= range2)
533 {
534 closest = enemy;
535 closestDistance = distance;
536 }
537 }
538 }
539 return closest;
540 }
541
542 int EnemyCount()
543 {
544 int count = 0;
545 for (int i = 0; i < enemyCount; i++)
546 {
547 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
548 {
549 count++;
550 }
551 }
552 return count;
553 }
554
555 void EnemyDrawHealthbars(Camera3D camera)
556 {
557 for (int i = 0; i < enemyCount; i++)
558 {
559 Enemy *enemy = &enemies[i];
560 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
561 {
562 continue;
563 }
564 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
565 float maxHealth = EnemyGetMaxHealth(enemy);
566 float health = maxHealth - enemy->damage;
567 float healthRatio = health / maxHealth;
568
569 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
570 }
571 }
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 if (unit.srcWeaponIdleFrameCount > 1)
120 {
121 int w = unit.srcWeaponIdleFrameWidth > 0 ? unit.srcWeaponIdleFrameWidth : srcRect.width;
122 srcRect.x += (int)(t / unit.srcWeaponIdleFrameDuration) % unit.srcWeaponIdleFrameCount * w;
123 }
124 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
125 }
126 }
127
128 void TowerInit()
129 {
130 for (int i = 0; i < TOWER_MAX_COUNT; i++)
131 {
132 towers[i] = (Tower){0};
133 }
134 towerCount = 0;
135
136 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
137 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
138
139 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
140 {
141 if (towerModels[i].materials)
142 {
143 // assign the palette texture to the material of the model (0 is not used afaik)
144 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
145 }
146 }
147 }
148
149 static void TowerGunUpdate(Tower *tower)
150 {
151 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
152 if (tower->cooldown <= 0.0f)
153 {
154 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
155 if (enemy)
156 {
157 tower->cooldown = config.cooldown;
158 // shoot the enemy; determine future position of the enemy
159 float bulletSpeed = config.projectileSpeed;
160 Vector2 velocity = enemy->simVelocity;
161 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
162 Vector2 towerPosition = {tower->x, tower->y};
163 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
164 for (int i = 0; i < 8; i++) {
165 velocity = enemy->simVelocity;
166 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
167 float distance = Vector2Distance(towerPosition, futurePosition);
168 float eta2 = distance / bulletSpeed;
169 if (fabs(eta - eta2) < 0.01f) {
170 break;
171 }
172 eta = (eta2 + eta) * 0.5f;
173 }
174
175 ProjectileTryAdd(config.projectileType, enemy,
176 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
177 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
178 bulletSpeed, config.hitEffect);
179 enemy->futureDamage += config.hitEffect.damage;
180 tower->lastTargetPosition = futurePosition;
181 }
182 }
183 else
184 {
185 tower->cooldown -= gameTime.deltaTime;
186 }
187 }
188
189 Tower *TowerGetAt(int16_t x, int16_t y)
190 {
191 for (int i = 0; i < towerCount; i++)
192 {
193 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
194 {
195 return &towers[i];
196 }
197 }
198 return 0;
199 }
200
201 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
202 {
203 if (towerCount >= TOWER_MAX_COUNT)
204 {
205 return 0;
206 }
207
208 Tower *tower = TowerGetAt(x, y);
209 if (tower)
210 {
211 return 0;
212 }
213
214 tower = &towers[towerCount++];
215 tower->x = x;
216 tower->y = y;
217 tower->towerType = towerType;
218 tower->cooldown = 0.0f;
219 tower->damage = 0.0f;
220 return tower;
221 }
222
223 Tower *GetTowerByType(uint8_t towerType)
224 {
225 for (int i = 0; i < towerCount; i++)
226 {
227 if (towers[i].towerType == towerType)
228 {
229 return &towers[i];
230 }
231 }
232 return 0;
233 }
234
235 int GetTowerCosts(uint8_t towerType)
236 {
237 return towerTypeConfigs[towerType].cost;
238 }
239
240 float TowerGetMaxHealth(Tower *tower)
241 {
242 return towerTypeConfigs[tower->towerType].maxHealth;
243 }
244
245 void TowerDrawSingle(Tower tower)
246 {
247 if (tower.towerType == TOWER_TYPE_NONE)
248 {
249 return;
250 }
251
252 switch (tower.towerType)
253 {
254 case TOWER_TYPE_ARCHER:
255 {
256 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
257 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
258 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
259 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
260 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
261 }
262 break;
263 case TOWER_TYPE_BALLISTA:
264 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
265 break;
266 case TOWER_TYPE_CATAPULT:
267 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
268 break;
269 default:
270 if (towerModels[tower.towerType].materials)
271 {
272 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
273 } else {
274 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
275 }
276 break;
277 }
278 }
279
280 void TowerDraw()
281 {
282 for (int i = 0; i < towerCount; i++)
283 {
284 TowerDrawSingle(towers[i]);
285 }
286 }
287
288 void TowerUpdate()
289 {
290 for (int i = 0; i < towerCount; i++)
291 {
292 Tower *tower = &towers[i];
293 switch (tower->towerType)
294 {
295 case TOWER_TYPE_CATAPULT:
296 case TOWER_TYPE_BALLISTA:
297 case TOWER_TYPE_ARCHER:
298 TowerGunUpdate(tower);
299 break;
300 }
301 }
302 }
303
304 void TowerDrawHealthBars(Camera3D camera)
305 {
306 for (int i = 0; i < towerCount; i++)
307 {
308 Tower *tower = &towers[i];
309 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
310 {
311 continue;
312 }
313
314 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
315 float maxHealth = TowerGetMaxHealth(tower);
316 float health = maxHealth - tower->damage;
317 float healthRatio = health / maxHealth;
318
319 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
320 }
321 }
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 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 weapon now has a simple animation in sync with the walking animation of the minion:
We can now use the minion without the weapon as the runner enemy. But maybe, we should refine the approach to describe sprite animations for the units. Just look at how the struct has grown:
1 typedef struct SpriteUnit
2 {
3 Rectangle srcRect;
4 Vector2 offset;
5 int frameCount;
6 float frameDuration;
7 Rectangle srcWeaponIdleRect;
8 Vector2 srcWeaponIdleOffset;
9 int srcWeaponIdleFrameCount;
10 int srcWeaponIdleFrameWidth;
11 float srcWeaponIdleFrameDuration;
12 Rectangle srcWeaponCooldownRect;
13 Vector2 srcWeaponCooldownOffset;
14 } SpriteUnit;
It isn't difficult to spot that there is a lot of repetition in the struct. It is basically a bunch of rectangles and offsets. Adding more and more such custom variables will make this struct and the code using it needlessly complex. So let's refactor this into a more generic struct that can handle multiple overlays and animations. A fixed number of overlays is good enough for our game:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {
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
21 #define ENEMY_TYPE_MINION 1
22 #define ENEMY_TYPE_RUNNER 2
23 #define ENEMY_TYPE_SHIELD 3
24 #define ENEMY_TYPE_BOSS 3
25
26 #define PARTICLE_MAX_COUNT 400
27 #define PARTICLE_TYPE_NONE 0
28 #define PARTICLE_TYPE_EXPLOSION 1
29
30 typedef struct Particle
31 {
32 uint8_t particleType;
33 float spawnTime;
34 float lifetime;
35 Vector3 position;
36 Vector3 velocity;
37 Vector3 scale;
38 } Particle;
39
40 #define TOWER_MAX_COUNT 400
41 enum TowerType
42 {
43 TOWER_TYPE_NONE,
44 TOWER_TYPE_BASE,
45 TOWER_TYPE_ARCHER,
46 TOWER_TYPE_BALLISTA,
47 TOWER_TYPE_CATAPULT,
48 TOWER_TYPE_WALL,
49 TOWER_TYPE_COUNT
50 };
51
52 typedef struct HitEffectConfig
53 {
54 float damage;
55 float areaDamageRadius;
56 float pushbackPowerDistance;
57 } HitEffectConfig;
58
59 typedef struct TowerTypeConfig
60 {
61 float cooldown;
62 float range;
63 float projectileSpeed;
64
65 uint8_t cost;
66 uint8_t projectileType;
67 uint16_t maxHealth;
68
69 HitEffectConfig hitEffect;
70 } TowerTypeConfig;
71
72 typedef struct Tower
73 {
74 int16_t x, y;
75 uint8_t towerType;
76 Vector2 lastTargetPosition;
77 float cooldown;
78 float damage;
79 } Tower;
80
81 typedef struct GameTime
82 {
83 float time;
84 float deltaTime;
85 uint32_t frameCount;
86
87 float fixedDeltaTime;
88 // leaving the fixed time stepping to the update functions,
89 // we need to know the fixed time at the start of the frame
90 float fixedTimeStart;
91 // and the number of fixed steps that we have to make this frame
92 // The fixedTime is fixedTimeStart + n * fixedStepCount
93 uint8_t fixedStepCount;
94 } GameTime;
95
96 typedef struct ButtonState {
97 char isSelected;
98 char isDisabled;
99 } ButtonState;
100
101 typedef struct GUIState {
102 int isBlocked;
103 } GUIState;
104
105 typedef enum LevelState
106 {
107 LEVEL_STATE_NONE,
108 LEVEL_STATE_BUILDING,
109 LEVEL_STATE_BUILDING_PLACEMENT,
110 LEVEL_STATE_BATTLE,
111 LEVEL_STATE_WON_WAVE,
112 LEVEL_STATE_LOST_WAVE,
113 LEVEL_STATE_WON_LEVEL,
114 LEVEL_STATE_RESET,
115 } LevelState;
116
117 typedef struct EnemyWave {
118 uint8_t enemyType;
119 uint8_t wave;
120 uint16_t count;
121 float interval;
122 float delay;
123 Vector2 spawnPosition;
124
125 uint16_t spawned;
126 float timeToSpawnNext;
127 } EnemyWave;
128
129 #define ENEMY_MAX_WAVE_COUNT 10
130
131 typedef enum PlacementPhase
132 {
133 PLACEMENT_PHASE_STARTING,
134 PLACEMENT_PHASE_MOVING,
135 PLACEMENT_PHASE_PLACING,
136 } PlacementPhase;
137
138 typedef struct Level
139 {
140 int seed;
141 LevelState state;
142 LevelState nextState;
143 Camera3D camera;
144 int placementMode;
145 PlacementPhase placementPhase;
146 float placementTimer;
147 int16_t placementX;
148 int16_t placementY;
149 Vector2 placementTransitionPosition;
150 PhysicsPoint placementTowerSpring;
151
152 int initialGold;
153 int playerGold;
154
155 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
156 int currentWave;
157 float waveEndTimer;
158 } Level;
159
160 typedef struct DeltaSrc
161 {
162 char x, y;
163 } DeltaSrc;
164
165 typedef struct PathfindingMap
166 {
167 int width, height;
168 float scale;
169 float *distances;
170 long *towerIndex;
171 DeltaSrc *deltaSrc;
172 float maxDistance;
173 Matrix toMapSpace;
174 Matrix toWorldSpace;
175 } PathfindingMap;
176
177 // when we execute the pathfinding algorithm, we need to store the active nodes
178 // in a queue. Each node has a position, a distance from the start, and the
179 // position of the node that we came from.
180 typedef struct PathfindingNode
181 {
182 int16_t x, y, fromX, fromY;
183 float distance;
184 } PathfindingNode;
185
186 typedef struct EnemyId
187 {
188 uint16_t index;
189 uint16_t generation;
190 } EnemyId;
191
192 typedef struct EnemyClassConfig
193 {
194 float speed;
195 float health;
196 float radius;
197 float maxAcceleration;
198 float requiredContactTime;
199 float explosionDamage;
200 float explosionRange;
201 float explosionPushbackPower;
202 int goldValue;
203 } EnemyClassConfig;
204
205 typedef struct Enemy
206 {
207 int16_t currentX, currentY;
208 int16_t nextX, nextY;
209 Vector2 simPosition;
210 Vector2 simVelocity;
211 uint16_t generation;
212 float walkedDistance;
213 float startMovingTime;
214 float damage, futureDamage;
215 float contactTime;
216 uint8_t enemyType;
217 uint8_t movePathCount;
218 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
219 } Enemy;
220
221 // a unit that uses sprites to be drawn
222 #define SPRITE_UNIT_ANIMATION_COUNT 6
223 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
224 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
225
226 typedef struct SpriteAnimation
227 {
228 Rectangle srcRect;
229 Vector2 offset;
230 uint8_t animationId;
231 uint8_t frameCount;
232 uint8_t frameWidth;
233 float frameDuration;
234 } SpriteAnimation;
235
236 typedef struct SpriteUnit
237 {
238 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
239 } SpriteUnit;
240
241 #define PROJECTILE_MAX_COUNT 1200
242 #define PROJECTILE_TYPE_NONE 0
243 #define PROJECTILE_TYPE_ARROW 1
244 #define PROJECTILE_TYPE_CATAPULT 2
245 #define PROJECTILE_TYPE_BALLISTA 3
246
247 typedef struct Projectile
248 {
249 uint8_t projectileType;
250 float shootTime;
251 float arrivalTime;
252 float distance;
253 Vector3 position;
254 Vector3 target;
255 Vector3 directionNormal;
256 EnemyId targetEnemy;
257 HitEffectConfig hitEffectConfig;
258 } Projectile;
259
260 //# Function declarations
261 float TowerGetMaxHealth(Tower *tower);
262 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
263 int EnemyAddDamageRange(Vector2 position, float range, float damage);
264 int EnemyAddDamage(Enemy *enemy, float damage);
265
266 //# Enemy functions
267 void EnemyInit();
268 void EnemyDraw();
269 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
270 void EnemyUpdate();
271 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
272 float EnemyGetMaxHealth(Enemy *enemy);
273 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
274 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
275 EnemyId EnemyGetId(Enemy *enemy);
276 Enemy *EnemyTryResolve(EnemyId enemyId);
277 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
278 int EnemyAddDamage(Enemy *enemy, float damage);
279 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
280 int EnemyCount();
281 void EnemyDrawHealthbars(Camera3D camera);
282
283 //# Tower functions
284 void TowerInit();
285 Tower *TowerGetAt(int16_t x, int16_t y);
286 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
287 Tower *GetTowerByType(uint8_t towerType);
288 int GetTowerCosts(uint8_t towerType);
289 float TowerGetMaxHealth(Tower *tower);
290 void TowerDraw();
291 void TowerDrawSingle(Tower tower);
292 void TowerUpdate();
293 void TowerDrawHealthBars(Camera3D camera);
294 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
295
296 //# Particles
297 void ParticleInit();
298 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
299 void ParticleUpdate();
300 void ParticleDraw();
301
302 //# Projectiles
303 void ProjectileInit();
304 void ProjectileDraw();
305 void ProjectileUpdate();
306 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
307
308 //# Pathfinding map
309 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
310 float PathFindingGetDistance(int mapX, int mapY);
311 Vector2 PathFindingGetGradient(Vector3 world);
312 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
313 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
314 void PathFindingMapDraw();
315
316 //# UI
317 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
318
319 //# Level
320 void DrawLevelGround(Level *level);
321 void DrawEnemyPath(Level *level, Color arrowColor);
322
323 //# variables
324 extern Level *currentLevel;
325 extern Enemy enemies[ENEMY_MAX_COUNT];
326 extern int enemyCount;
327 extern EnemyClassConfig enemyClassConfigs[];
328
329 extern GUIState guiState;
330 extern GameTime gameTime;
331 extern Tower towers[TOWER_MAX_COUNT];
332 extern int towerCount;
333
334 extern Texture2D palette, spriteSheet;
335
336 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 };
31
32 Enemy enemies[ENEMY_MAX_COUNT];
33 int enemyCount = 0;
34
35 SpriteUnit enemySprites[] = {
36 [ENEMY_TYPE_MINION] = {
37 .animations[0] = {
38 .srcRect = {0, 17, 16, 15},
39 .offset = {8.0f, 0.0f},
40 .frameCount = 6,
41 .frameDuration = 0.1f,
42 },
43 .animations[1] = {
44 .srcRect = {1, 33, 15, 14},
45 .offset = {7.0f, 0.0f},
46 .frameCount = 6,
47 .frameWidth = 16,
48 .frameDuration = 0.1f,
49 },
50 },
51 [ENEMY_TYPE_RUNNER] = {
52 .animations[0] = {
53 .srcRect = {0, 17, 16, 15},
54 .offset = {8.0f, 0.0f},
55 .frameCount = 6,
56 .frameDuration = 0.1f,
57 },
58 },
59 };
60
61 void EnemyInit()
62 {
63 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
64 {
65 enemies[i] = (Enemy){0};
66 }
67 enemyCount = 0;
68 }
69
70 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
71 {
72 return enemyClassConfigs[enemy->enemyType].speed;
73 }
74
75 float EnemyGetMaxHealth(Enemy *enemy)
76 {
77 return enemyClassConfigs[enemy->enemyType].health;
78 }
79
80 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
81 {
82 int16_t castleX = 0;
83 int16_t castleY = 0;
84 int16_t dx = castleX - currentX;
85 int16_t dy = castleY - currentY;
86 if (dx == 0 && dy == 0)
87 {
88 *nextX = currentX;
89 *nextY = currentY;
90 return 1;
91 }
92 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
93
94 if (gradient.x == 0 && gradient.y == 0)
95 {
96 *nextX = currentX;
97 *nextY = currentY;
98 return 1;
99 }
100
101 if (fabsf(gradient.x) > fabsf(gradient.y))
102 {
103 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
104 *nextY = currentY;
105 return 0;
106 }
107 *nextX = currentX;
108 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
109 return 0;
110 }
111
112
113 // this function predicts the movement of the unit for the next deltaT seconds
114 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
115 {
116 const float pointReachedDistance = 0.25f;
117 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
118 const float maxSimStepTime = 0.015625f;
119
120 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
121 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
122 int16_t nextX = enemy->nextX;
123 int16_t nextY = enemy->nextY;
124 Vector2 position = enemy->simPosition;
125 int passedCount = 0;
126 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
127 {
128 float stepTime = fminf(deltaT - t, maxSimStepTime);
129 Vector2 target = (Vector2){nextX, nextY};
130 float speed = Vector2Length(*velocity);
131 // draw the target position for debugging
132 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
133 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
134 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
135 {
136 // we reached the target position, let's move to the next waypoint
137 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
138 target = (Vector2){nextX, nextY};
139 // track how many waypoints we passed
140 passedCount++;
141 }
142
143 // acceleration towards the target
144 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
145 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
146 *velocity = Vector2Add(*velocity, acceleration);
147
148 // limit the speed to the maximum speed
149 if (speed > maxSpeed)
150 {
151 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
152 }
153
154 // move the enemy
155 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
156 }
157
158 if (waypointPassedCount)
159 {
160 (*waypointPassedCount) = passedCount;
161 }
162
163 return position;
164 }
165
166 void EnemyDraw()
167 {
168 rlDrawRenderBatchActive();
169 rlDisableDepthMask();
170 for (int i = 0; i < enemyCount; i++)
171 {
172 Enemy enemy = enemies[i];
173 if (enemy.enemyType == ENEMY_TYPE_NONE)
174 {
175 continue;
176 }
177
178 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
179
180 // don't draw any trails for now; might replace this with footprints later
181 // if (enemy.movePathCount > 0)
182 // {
183 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
184 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
185 // }
186 // for (int j = 1; j < enemy.movePathCount; j++)
187 // {
188 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
189 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
190 // DrawLine3D(p, q, GREEN);
191 // }
192
193 switch (enemy.enemyType)
194 {
195 case ENEMY_TYPE_MINION:
196 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
197 enemy.walkedDistance, 0, 0);
198 break;
199 }
200 }
201 rlDrawRenderBatchActive();
202 rlEnableDepthMask();
203 }
204
205 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
206 {
207 // damage the tower
208 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
209 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
210 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
211 float explosionRange2 = explosionRange * explosionRange;
212 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
213 // explode the enemy
214 if (tower->damage >= TowerGetMaxHealth(tower))
215 {
216 tower->towerType = TOWER_TYPE_NONE;
217 }
218
219 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
220 explosionSource,
221 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
222
223 enemy->enemyType = ENEMY_TYPE_NONE;
224
225 // push back enemies & dealing damage
226 for (int i = 0; i < enemyCount; i++)
227 {
228 Enemy *other = &enemies[i];
229 if (other->enemyType == ENEMY_TYPE_NONE)
230 {
231 continue;
232 }
233 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
234 if (distanceSqr > 0 && distanceSqr < explosionRange2)
235 {
236 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
237 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
238 EnemyAddDamage(other, explosionDamge);
239 }
240 }
241 }
242
243 void EnemyUpdate()
244 {
245 const float castleX = 0;
246 const float castleY = 0;
247 const float maxPathDistance2 = 0.25f * 0.25f;
248
249 for (int i = 0; i < enemyCount; i++)
250 {
251 Enemy *enemy = &enemies[i];
252 if (enemy->enemyType == ENEMY_TYPE_NONE)
253 {
254 continue;
255 }
256
257 int waypointPassedCount = 0;
258 Vector2 prevPosition = enemy->simPosition;
259 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
260 enemy->startMovingTime = gameTime.time;
261 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
262 // track path of unit
263 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
264 {
265 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
266 {
267 enemy->movePath[j] = enemy->movePath[j - 1];
268 }
269 enemy->movePath[0] = enemy->simPosition;
270 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
271 {
272 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
273 }
274 }
275
276 if (waypointPassedCount > 0)
277 {
278 enemy->currentX = enemy->nextX;
279 enemy->currentY = enemy->nextY;
280 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
281 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
282 {
283 // enemy reached the castle; remove it
284 enemy->enemyType = ENEMY_TYPE_NONE;
285 continue;
286 }
287 }
288 }
289
290 // handle collisions between enemies
291 for (int i = 0; i < enemyCount - 1; i++)
292 {
293 Enemy *enemyA = &enemies[i];
294 if (enemyA->enemyType == ENEMY_TYPE_NONE)
295 {
296 continue;
297 }
298 for (int j = i + 1; j < enemyCount; j++)
299 {
300 Enemy *enemyB = &enemies[j];
301 if (enemyB->enemyType == ENEMY_TYPE_NONE)
302 {
303 continue;
304 }
305 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
306 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
307 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
308 float radiusSum = radiusA + radiusB;
309 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
310 {
311 // collision
312 float distance = sqrtf(distanceSqr);
313 float overlap = radiusSum - distance;
314 // move the enemies apart, but softly; if we have a clog of enemies,
315 // moving them perfectly apart can cause them to jitter
316 float positionCorrection = overlap / 5.0f;
317 Vector2 direction = (Vector2){
318 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
319 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
320 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
321 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
322 }
323 }
324 }
325
326 // handle collisions between enemies and towers
327 for (int i = 0; i < enemyCount; i++)
328 {
329 Enemy *enemy = &enemies[i];
330 if (enemy->enemyType == ENEMY_TYPE_NONE)
331 {
332 continue;
333 }
334 enemy->contactTime -= gameTime.deltaTime;
335 if (enemy->contactTime < 0.0f)
336 {
337 enemy->contactTime = 0.0f;
338 }
339
340 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
341 // linear search over towers; could be optimized by using path finding tower map,
342 // but for now, we keep it simple
343 for (int j = 0; j < towerCount; j++)
344 {
345 Tower *tower = &towers[j];
346 if (tower->towerType == TOWER_TYPE_NONE)
347 {
348 continue;
349 }
350 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
351 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
352 if (distanceSqr > combinedRadius * combinedRadius)
353 {
354 continue;
355 }
356 // potential collision; square / circle intersection
357 float dx = tower->x - enemy->simPosition.x;
358 float dy = tower->y - enemy->simPosition.y;
359 float absDx = fabsf(dx);
360 float absDy = fabsf(dy);
361 Vector3 contactPoint = {0};
362 if (absDx <= 0.5f && absDx <= absDy) {
363 // vertical collision; push the enemy out horizontally
364 float overlap = enemyRadius + 0.5f - absDy;
365 if (overlap < 0.0f)
366 {
367 continue;
368 }
369 float direction = dy > 0.0f ? -1.0f : 1.0f;
370 enemy->simPosition.y += direction * overlap;
371 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
372 }
373 else if (absDy <= 0.5f && absDy <= absDx)
374 {
375 // horizontal collision; push the enemy out vertically
376 float overlap = enemyRadius + 0.5f - absDx;
377 if (overlap < 0.0f)
378 {
379 continue;
380 }
381 float direction = dx > 0.0f ? -1.0f : 1.0f;
382 enemy->simPosition.x += direction * overlap;
383 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
384 }
385 else
386 {
387 // possible collision with a corner
388 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
389 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
390 float cornerX = tower->x + cornerDX;
391 float cornerY = tower->y + cornerDY;
392 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
393 if (cornerDistanceSqr > enemyRadius * enemyRadius)
394 {
395 continue;
396 }
397 // push the enemy out along the diagonal
398 float cornerDistance = sqrtf(cornerDistanceSqr);
399 float overlap = enemyRadius - cornerDistance;
400 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
401 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
402 enemy->simPosition.x -= directionX * overlap;
403 enemy->simPosition.y -= directionY * overlap;
404 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
405 }
406
407 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
408 {
409 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
410 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
411 {
412 EnemyTriggerExplode(enemy, tower, contactPoint);
413 }
414 }
415 }
416 }
417 }
418
419 EnemyId EnemyGetId(Enemy *enemy)
420 {
421 return (EnemyId){enemy - enemies, enemy->generation};
422 }
423
424 Enemy *EnemyTryResolve(EnemyId enemyId)
425 {
426 if (enemyId.index >= ENEMY_MAX_COUNT)
427 {
428 return 0;
429 }
430 Enemy *enemy = &enemies[enemyId.index];
431 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
432 {
433 return 0;
434 }
435 return enemy;
436 }
437
438 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
439 {
440 Enemy *spawn = 0;
441 for (int i = 0; i < enemyCount; i++)
442 {
443 Enemy *enemy = &enemies[i];
444 if (enemy->enemyType == ENEMY_TYPE_NONE)
445 {
446 spawn = enemy;
447 break;
448 }
449 }
450
451 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
452 {
453 spawn = &enemies[enemyCount++];
454 }
455
456 if (spawn)
457 {
458 spawn->currentX = currentX;
459 spawn->currentY = currentY;
460 spawn->nextX = currentX;
461 spawn->nextY = currentY;
462 spawn->simPosition = (Vector2){currentX, currentY};
463 spawn->simVelocity = (Vector2){0, 0};
464 spawn->enemyType = enemyType;
465 spawn->startMovingTime = gameTime.time;
466 spawn->damage = 0.0f;
467 spawn->futureDamage = 0.0f;
468 spawn->generation++;
469 spawn->movePathCount = 0;
470 spawn->walkedDistance = 0.0f;
471 }
472
473 return spawn;
474 }
475
476 int EnemyAddDamageRange(Vector2 position, float range, float damage)
477 {
478 int count = 0;
479 float range2 = range * range;
480 for (int i = 0; i < enemyCount; i++)
481 {
482 Enemy *enemy = &enemies[i];
483 if (enemy->enemyType == ENEMY_TYPE_NONE)
484 {
485 continue;
486 }
487 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
488 if (distance2 <= range2)
489 {
490 EnemyAddDamage(enemy, damage);
491 count++;
492 }
493 }
494 return count;
495 }
496
497 int EnemyAddDamage(Enemy *enemy, float damage)
498 {
499 enemy->damage += damage;
500 if (enemy->damage >= EnemyGetMaxHealth(enemy))
501 {
502 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
503 enemy->enemyType = ENEMY_TYPE_NONE;
504 return 1;
505 }
506
507 return 0;
508 }
509
510 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
511 {
512 int16_t castleX = 0;
513 int16_t castleY = 0;
514 Enemy* closest = 0;
515 int16_t closestDistance = 0;
516 float range2 = range * range;
517 for (int i = 0; i < enemyCount; i++)
518 {
519 Enemy* enemy = &enemies[i];
520 if (enemy->enemyType == ENEMY_TYPE_NONE)
521 {
522 continue;
523 }
524 float maxHealth = EnemyGetMaxHealth(enemy);
525 if (enemy->futureDamage >= maxHealth)
526 {
527 // ignore enemies that will die soon
528 continue;
529 }
530 int16_t dx = castleX - enemy->currentX;
531 int16_t dy = castleY - enemy->currentY;
532 int16_t distance = abs(dx) + abs(dy);
533 if (!closest || distance < closestDistance)
534 {
535 float tdx = towerX - enemy->currentX;
536 float tdy = towerY - enemy->currentY;
537 float tdistance2 = tdx * tdx + tdy * tdy;
538 if (tdistance2 <= range2)
539 {
540 closest = enemy;
541 closestDistance = distance;
542 }
543 }
544 }
545 return closest;
546 }
547
548 int EnemyCount()
549 {
550 int count = 0;
551 for (int i = 0; i < enemyCount; i++)
552 {
553 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
554 {
555 count++;
556 }
557 }
558 return count;
559 }
560
561 void EnemyDrawHealthbars(Camera3D camera)
562 {
563 for (int i = 0; i < enemyCount; i++)
564 {
565 Enemy *enemy = &enemies[i];
566 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
567 {
568 continue;
569 }
570 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
571 float maxHealth = EnemyGetMaxHealth(enemy);
572 float health = maxHealth - enemy->damage;
573 float healthRatio = health / maxHealth;
574
575 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
576 }
577 }
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 .animations[0] = {
57 .srcRect = {0, 0, 16, 16},
58 .offset = {7, 1},
59 .frameCount = 1,
60 .frameDuration = 0.0f,
61 },
62 .animations[1] = {
63 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
64 .srcRect = {16, 0, 6, 16},
65 .offset = {8, 0},
66 },
67 .animations[2] = {
68 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
69 .srcRect = {22, 0, 11, 16},
70 .offset = {10, 0},
71 },
72 };
73
74 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
75 {
76 float xScale = flip ? -1.0f : 1.0f;
77 Camera3D camera = currentLevel->camera;
78 float size = 0.5f;
79 // we want the sprite to face the camera, so we need to calculate the up vector
80 Vector3 forward = Vector3Subtract(camera.target, camera.position);
81 Vector3 up = {0, 1, 0};
82 Vector3 right = Vector3CrossProduct(forward, up);
83 up = Vector3Normalize(Vector3CrossProduct(right, forward));
84
85 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
86 {
87 SpriteAnimation anim = unit.animations[i];
88 if (anim.animationId != phase && anim.animationId != 0)
89 {
90 continue;
91 }
92 Rectangle srcRect = anim.srcRect;
93 if (anim.frameCount > 1)
94 {
95 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
96 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
97 }
98 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
99 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
100
101 if (flip)
102 {
103 srcRect.x += srcRect.width;
104 srcRect.width = -srcRect.width;
105 offset.x = scale.x - offset.x;
106 }
107 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
108 // move the sprite slightly towards the camera to avoid z-fighting
109 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
110 }
111 }
112
113 void TowerInit()
114 {
115 for (int i = 0; i < TOWER_MAX_COUNT; i++)
116 {
117 towers[i] = (Tower){0};
118 }
119 towerCount = 0;
120
121 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
122 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
123
124 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
125 {
126 if (towerModels[i].materials)
127 {
128 // assign the palette texture to the material of the model (0 is not used afaik)
129 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
130 }
131 }
132 }
133
134 static void TowerGunUpdate(Tower *tower)
135 {
136 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
137 if (tower->cooldown <= 0.0f)
138 {
139 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
140 if (enemy)
141 {
142 tower->cooldown = config.cooldown;
143 // shoot the enemy; determine future position of the enemy
144 float bulletSpeed = config.projectileSpeed;
145 Vector2 velocity = enemy->simVelocity;
146 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
147 Vector2 towerPosition = {tower->x, tower->y};
148 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
149 for (int i = 0; i < 8; i++) {
150 velocity = enemy->simVelocity;
151 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
152 float distance = Vector2Distance(towerPosition, futurePosition);
153 float eta2 = distance / bulletSpeed;
154 if (fabs(eta - eta2) < 0.01f) {
155 break;
156 }
157 eta = (eta2 + eta) * 0.5f;
158 }
159
160 ProjectileTryAdd(config.projectileType, enemy,
161 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
162 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
163 bulletSpeed, config.hitEffect);
164 enemy->futureDamage += config.hitEffect.damage;
165 tower->lastTargetPosition = futurePosition;
166 }
167 }
168 else
169 {
170 tower->cooldown -= gameTime.deltaTime;
171 }
172 }
173
174 Tower *TowerGetAt(int16_t x, int16_t y)
175 {
176 for (int i = 0; i < towerCount; i++)
177 {
178 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
179 {
180 return &towers[i];
181 }
182 }
183 return 0;
184 }
185
186 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
187 {
188 if (towerCount >= TOWER_MAX_COUNT)
189 {
190 return 0;
191 }
192
193 Tower *tower = TowerGetAt(x, y);
194 if (tower)
195 {
196 return 0;
197 }
198
199 tower = &towers[towerCount++];
200 tower->x = x;
201 tower->y = y;
202 tower->towerType = towerType;
203 tower->cooldown = 0.0f;
204 tower->damage = 0.0f;
205 return tower;
206 }
207
208 Tower *GetTowerByType(uint8_t towerType)
209 {
210 for (int i = 0; i < towerCount; i++)
211 {
212 if (towers[i].towerType == towerType)
213 {
214 return &towers[i];
215 }
216 }
217 return 0;
218 }
219
220 int GetTowerCosts(uint8_t towerType)
221 {
222 return towerTypeConfigs[towerType].cost;
223 }
224
225 float TowerGetMaxHealth(Tower *tower)
226 {
227 return towerTypeConfigs[tower->towerType].maxHealth;
228 }
229
230 void TowerDrawSingle(Tower tower)
231 {
232 if (tower.towerType == TOWER_TYPE_NONE)
233 {
234 return;
235 }
236
237 switch (tower.towerType)
238 {
239 case TOWER_TYPE_ARCHER:
240 {
241 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
242 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
243 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
244 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
245 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
246 }
247 break;
248 case TOWER_TYPE_BALLISTA:
249 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
250 break;
251 case TOWER_TYPE_CATAPULT:
252 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
253 break;
254 default:
255 if (towerModels[tower.towerType].materials)
256 {
257 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
258 } else {
259 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
260 }
261 break;
262 }
263 }
264
265 void TowerDraw()
266 {
267 for (int i = 0; i < towerCount; i++)
268 {
269 TowerDrawSingle(towers[i]);
270 }
271 }
272
273 void TowerUpdate()
274 {
275 for (int i = 0; i < towerCount; i++)
276 {
277 Tower *tower = &towers[i];
278 switch (tower->towerType)
279 {
280 case TOWER_TYPE_CATAPULT:
281 case TOWER_TYPE_BALLISTA:
282 case TOWER_TYPE_ARCHER:
283 TowerGunUpdate(tower);
284 break;
285 }
286 }
287 }
288
289 void TowerDrawHealthBars(Camera3D camera)
290 {
291 for (int i = 0; i < towerCount; i++)
292 {
293 Tower *tower = &towers[i];
294 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
295 {
296 continue;
297 }
298
299 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
300 float maxHealth = TowerGetMaxHealth(tower);
301 float health = maxHealth - tower->damage;
302 float healthRatio = health / maxHealth;
303
304 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
305 }
306 }
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 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
There is now a new SpriteAnimation struct that describes a single sprite animation source:
1 typedef struct SpriteAnimation
2 {
3 Rectangle srcRect;
4 Vector2 offset;
5 uint8_t animationId;
6 uint8_t frameCount;
7 uint8_t frameWidth;
8 float frameDuration;
9 } SpriteAnimation;
This used to be a part of the SpriteUnit - which looks now very simple:
1 typedef struct SpriteUnit
2 {
3 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
4 } SpriteUnit;
It is now greatly simplified and is just a number of animations. Creating animations for a unit is now a description of the different animations that are drawn on top of each other:
1 SpriteUnit enemySprites[] = {
2 [ENEMY_TYPE_MINION] = {
3 .animations[0] = { // the minion body
4 .srcRect = {0, 17, 16, 15},
5 .offset = {8.0f, 0.0f},
6 .frameCount = 6,
7 .frameDuration = 0.1f,
8 },
9 .animations[1] = { // the sword
10 .srcRect = {1, 33, 15, 14},
11 .offset = {7.0f, 0.0f},
12 .frameCount = 6,
13 .frameWidth = 16,
14 .frameDuration = 0.1f,
15 },
16 },
17 ...
18 }
It is simple to see that with this approach that we can make up enemies with multiple overlays and animations. But the best part is how the code looks like that draws the unit sprite - let's compare it - first the new version:
1 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
2 {
3 float xScale = flip ? -1.0f : 1.0f;
4 Camera3D camera = currentLevel->camera;
5 float size = 0.5f;
6 // we want the sprite to face the camera, so we need to calculate the up vector
7 Vector3 forward = Vector3Subtract(camera.target, camera.position);
8 Vector3 up = {0, 1, 0};
9 Vector3 right = Vector3CrossProduct(forward, up);
10 up = Vector3Normalize(Vector3CrossProduct(right, forward));
11
12 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
13 {
14 SpriteAnimation anim = unit.animations[i];
15 if (anim.animationId != phase && anim.animationId != 0)
16 {
17 continue;
18 }
19 Rectangle srcRect = anim.srcRect;
20 if (anim.frameCount > 1)
21 {
22 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
23 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
24 }
25 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
26 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
27
28 if (flip)
29 {
30 srcRect.x += srcRect.width;
31 srcRect.width = -srcRect.width;
32 offset.x = scale.x - offset.x;
33 }
34 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
35 // move the sprite slightly towards the camera to avoid z-fighting
36 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
37 }
38 }
Noteworthy is this part:
if (anim.animationId != phase && anim.animationId != 0) { continue; }
... which is ignoring SpriteAnimation elements that have an animationId set and that does not match the current phase. This is how we can have units that draw different animations depending on the phase of the unit - e.g. for the archers. Not the most elegant approach, but it works for now!
And the old version:
1 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
2 {
3 float xScale = flip ? -1.0f : 1.0f;
4 Camera3D camera = currentLevel->camera;
5 float size = 0.5f;
6 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
7 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
8 // we want the sprite to face the camera, so we need to calculate the up vector
9 Vector3 forward = Vector3Subtract(camera.target, camera.position);
10 Vector3 up = {0, 1, 0};
11 Vector3 right = Vector3CrossProduct(forward, up);
12 up = Vector3Normalize(Vector3CrossProduct(right, forward));
13
14 Rectangle srcRect = unit.srcRect;
15 if (unit.frameCount > 1)
16 {
17 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
18 }
19 if (flip)
20 {
21 srcRect.x += srcRect.width;
22 srcRect.width = -srcRect.width;
23 }
24 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
25 // move the sprite slightly towards the camera to avoid z-fighting
26 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
27
28 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
29 {
30 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
31 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
32 srcRect = unit.srcWeaponCooldownRect;
33 if (flip)
34 {
35 // position.x = flip * scale.x * 0.5f;
36 srcRect.x += srcRect.width;
37 srcRect.width = -srcRect.width;
38 offset.x = scale.x - offset.x;
39 }
40 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
41 }
42 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
43 {
44 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
45 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
46 srcRect = unit.srcWeaponIdleRect;
47 if (flip)
48 {
49 // position.x = flip * scale.x * 0.5f;
50 srcRect.x += srcRect.width;
51 srcRect.width = -srcRect.width;
52 offset.x = scale.x - offset.x;
53 }
54 if (unit.srcWeaponIdleFrameCount > 1)
55 {
56 int w = unit.srcWeaponIdleFrameWidth > 0 ? unit.srcWeaponIdleFrameWidth : srcRect.width;
57 srcRect.x += (int)(t / unit.srcWeaponIdleFrameDuration) % unit.srcWeaponIdleFrameCount * w;
58 }
59 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
60 }
Comparing the changes, we can see that setting up a unit sprite is more structured now and the code to draw it has become significantly shorter and simpler.
Both should be useful when adding more units and features to the game. We might later want to add more effects to the sprite animations, like scaling, rotation, or color changes and with this approach, this should be easier to do. Later.
Now, let's add the new enemies and let them spawn to see how they look like.
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_RUNNER,
39 .wave = 0,
40 .count = 5,
41 .interval = 2.5f,
42 .delay = 1.0f,
43 .spawnPosition = {-2, 6},
44 },
45 .waves[2] = {
46 .enemyType = ENEMY_TYPE_SHIELD,
47 .wave = 1,
48 .count = 20,
49 .interval = 1.5f,
50 .delay = 1.0f,
51 .spawnPosition = {0, 6},
52 },
53 .waves[3] = {
54 .enemyType = ENEMY_TYPE_MINION,
55 .wave = 2,
56 .count = 30,
57 .interval = 1.2f,
58 .delay = 1.0f,
59 .spawnPosition = {2, 6},
60 },
61 .waves[4] = {
62 .enemyType = ENEMY_TYPE_BOSS,
63 .wave = 0,
64 .count = 2,
65 .interval = 5.0f,
66 .delay = 2.0f,
67 .spawnPosition = {-2, 4},
68 }
69 },
70 };
71
72 Level *currentLevel = levels;
73
74 //# Game
75
76 static Model LoadGLBModel(char *filename)
77 {
78 Model model = LoadModel(TextFormat("data/%s.glb",filename));
79 for (int i = 0; i < model.materialCount; i++)
80 {
81 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
82 }
83 return model;
84 }
85
86 void LoadAssets()
87 {
88 // load a sprite sheet that contains all units
89 spriteSheet = LoadTexture("data/spritesheet.png");
90 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
91
92 // we'll use a palette texture to colorize the all buildings and environment art
93 palette = LoadTexture("data/palette.png");
94 // The texture uses gradients on very small space, so we'll enable bilinear filtering
95 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
96
97 floorTileAModel = LoadGLBModel("floor-tile-a");
98 floorTileBModel = LoadGLBModel("floor-tile-b");
99 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
100 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
101 firTreeModel[0] = LoadGLBModel("firtree-1-a");
102 firTreeModel[1] = LoadGLBModel("firtree-1-b");
103 rockModels[0] = LoadGLBModel("rock-1");
104 rockModels[1] = LoadGLBModel("rock-2");
105 rockModels[2] = LoadGLBModel("rock-3");
106 rockModels[3] = LoadGLBModel("rock-4");
107 rockModels[4] = LoadGLBModel("rock-5");
108 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
109
110 pathArrowModel = LoadGLBModel("direction-arrow-x");
111 greenArrowModel = LoadGLBModel("green-arrow");
112 }
113
114 void InitLevel(Level *level)
115 {
116 level->seed = (int)(GetTime() * 100.0f);
117
118 TowerInit();
119 EnemyInit();
120 ProjectileInit();
121 ParticleInit();
122 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
123
124 level->placementMode = 0;
125 level->state = LEVEL_STATE_BUILDING;
126 level->nextState = LEVEL_STATE_NONE;
127 level->playerGold = level->initialGold;
128 level->currentWave = 0;
129 level->placementX = -1;
130 level->placementY = 0;
131
132 Camera *camera = &level->camera;
133 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
134 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
135 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
136 camera->fovy = 10.0f;
137 camera->projection = CAMERA_ORTHOGRAPHIC;
138 }
139
140 void DrawLevelHud(Level *level)
141 {
142 const char *text = TextFormat("Gold: %d", level->playerGold);
143 Font font = GetFontDefault();
144 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
145 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
146 }
147
148 void DrawLevelReportLostWave(Level *level)
149 {
150 BeginMode3D(level->camera);
151 DrawLevelGround(level);
152 TowerDraw();
153 EnemyDraw();
154 ProjectileDraw();
155 ParticleDraw();
156 guiState.isBlocked = 0;
157 EndMode3D();
158
159 TowerDrawHealthBars(level->camera);
160
161 const char *text = "Wave lost";
162 int textWidth = MeasureText(text, 20);
163 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
164
165 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
166 {
167 level->nextState = LEVEL_STATE_RESET;
168 }
169 }
170
171 int HasLevelNextWave(Level *level)
172 {
173 for (int i = 0; i < 10; i++)
174 {
175 EnemyWave *wave = &level->waves[i];
176 if (wave->wave == level->currentWave)
177 {
178 return 1;
179 }
180 }
181 return 0;
182 }
183
184 void DrawLevelReportWonWave(Level *level)
185 {
186 BeginMode3D(level->camera);
187 DrawLevelGround(level);
188 TowerDraw();
189 EnemyDraw();
190 ProjectileDraw();
191 ParticleDraw();
192 guiState.isBlocked = 0;
193 EndMode3D();
194
195 TowerDrawHealthBars(level->camera);
196
197 const char *text = "Wave won";
198 int textWidth = MeasureText(text, 20);
199 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
200
201
202 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
203 {
204 level->nextState = LEVEL_STATE_RESET;
205 }
206
207 if (HasLevelNextWave(level))
208 {
209 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
210 {
211 level->nextState = LEVEL_STATE_BUILDING;
212 }
213 }
214 else {
215 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
216 {
217 level->nextState = LEVEL_STATE_WON_LEVEL;
218 }
219 }
220 }
221
222 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
223 {
224 static ButtonState buttonStates[8] = {0};
225 int cost = GetTowerCosts(towerType);
226 const char *text = TextFormat("%s: %d", name, cost);
227 buttonStates[towerType].isSelected = level->placementMode == towerType;
228 buttonStates[towerType].isDisabled = level->playerGold < cost;
229 if (Button(text, x, y, width, height, &buttonStates[towerType]))
230 {
231 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
232 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
233 }
234 }
235
236 float GetRandomFloat(float min, float max)
237 {
238 int random = GetRandomValue(0, 0xfffffff);
239 return ((float)random / (float)0xfffffff) * (max - min) + min;
240 }
241
242 void DrawLevelGround(Level *level)
243 {
244 // draw checkerboard ground pattern
245 for (int x = -5; x <= 5; x += 1)
246 {
247 for (int y = -5; y <= 5; y += 1)
248 {
249 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
250 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
251 }
252 }
253
254 int oldSeed = GetRandomValue(0, 0xfffffff);
255 SetRandomSeed(level->seed);
256 // increase probability for trees via duplicated entries
257 Model borderModels[64];
258 int maxRockCount = GetRandomValue(2, 6);
259 int maxTreeCount = GetRandomValue(10, 20);
260 int maxFirTreeCount = GetRandomValue(5, 10);
261 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
262 int grassPatchCount = GetRandomValue(5, 30);
263
264 int modelCount = 0;
265 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
266 {
267 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
268 }
269 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
270 {
271 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
272 }
273 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
274 {
275 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
276 }
277 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
278 {
279 borderModels[modelCount++] = grassPatchModel[0];
280 }
281
282 // draw some objects around the border of the map
283 Vector3 up = {0, 1, 0};
284 // a pseudo random number generator to get the same result every time
285 const float wiggle = 0.75f;
286 const int layerCount = 3;
287 for (int layer = 0; layer < layerCount; layer++)
288 {
289 int layerPos = 6 + layer;
290 for (int x = -6 + layer; x <= 6 + layer; x += 1)
291 {
292 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
293 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
294 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
295 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
296 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
297 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
298 }
299
300 for (int z = -5 + layer; z <= 5 + layer; z += 1)
301 {
302 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
303 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
304 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
305 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
306 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
307 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
308 }
309 }
310
311 SetRandomSeed(oldSeed);
312 }
313
314 void DrawEnemyPath(Level *level, Color arrowColor)
315 {
316 const int castleX = 0, castleY = 0;
317 const int maxWaypointCount = 200;
318 const float timeStep = 1.0f;
319 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
320
321 // we start with a time offset to simulate the path,
322 // this way the arrows are animated in a forward moving direction
323 // The time is wrapped around the time step to get a smooth animation
324 float timeOffset = fmodf(GetTime(), timeStep);
325
326 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
327 {
328 EnemyWave *wave = &level->waves[i];
329 if (wave->wave != level->currentWave)
330 {
331 continue;
332 }
333
334 // use this dummy enemy to simulate the path
335 Enemy dummy = {
336 .enemyType = ENEMY_TYPE_MINION,
337 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
338 .nextX = wave->spawnPosition.x,
339 .nextY = wave->spawnPosition.y,
340 .currentX = wave->spawnPosition.x,
341 .currentY = wave->spawnPosition.y,
342 };
343
344 float deltaTime = timeOffset;
345 for (int j = 0; j < maxWaypointCount; j++)
346 {
347 int waypointPassedCount = 0;
348 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
349 // after the initial variable starting offset, we use a fixed time step
350 deltaTime = timeStep;
351 dummy.simPosition = pos;
352
353 // Update the dummy's position just like we do in the regular enemy update loop
354 for (int k = 0; k < waypointPassedCount; k++)
355 {
356 dummy.currentX = dummy.nextX;
357 dummy.currentY = dummy.nextY;
358 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
359 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
360 {
361 break;
362 }
363 }
364 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
365 {
366 break;
367 }
368
369 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
370 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
371 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
372 }
373 }
374 }
375
376 void DrawEnemyPaths(Level *level)
377 {
378 // disable depth testing for the path arrows
379 // flush the 3D batch to draw the arrows on top of everything
380 rlDrawRenderBatchActive();
381 rlDisableDepthTest();
382 DrawEnemyPath(level, (Color){64, 64, 64, 160});
383
384 rlDrawRenderBatchActive();
385 rlEnableDepthTest();
386 DrawEnemyPath(level, WHITE);
387 }
388
389 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
390 {
391 float dt = gameTime.fixedDeltaTime;
392 // smooth transition for the placement position using exponential decay
393 const float lambda = 15.0f;
394 float factor = 1.0f - expf(-lambda * dt);
395
396 float damping = 0.5f;
397 float springStiffness = 300.0f;
398 float springDecay = 95.0f;
399 float minHeight = 0.35f;
400
401 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
402 {
403 damping = 1.0f;
404 springDecay = 90.0f;
405 springStiffness = 100.0f;
406 minHeight = 0.70f;
407 }
408
409 for (int i = 0; i < gameTime.fixedStepCount; i++)
410 {
411 level->placementTransitionPosition =
412 Vector2Lerp(
413 level->placementTransitionPosition,
414 (Vector2){mapX, mapY}, factor);
415
416 // draw the spring position for debugging the spring simulation
417 // first step: stiff spring, no simulation
418 Vector3 worldPlacementPosition = (Vector3){
419 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
420 Vector3 springTargetPosition = (Vector3){
421 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
422 // consider the current velocity to predict the future position in order to dampen
423 // the spring simulation. Longer prediction times will result in more damping
424 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
425 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
426 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
427 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
428 // decay velocity of the upright forcing spring
429 // This force acts like a 2nd spring that pulls the tip upright into the air above the
430 // base position
431 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
432 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
433
434 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
435 // we use a simple spring model with a rest length of 1.0f
436 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
437 float springLength = Vector3Length(springDelta);
438 float springForce = (springLength - 1.0f) * springStiffness;
439 Vector3 springForceVector = Vector3Normalize(springDelta);
440 springForceVector = Vector3Scale(springForceVector, springForce);
441 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
442 Vector3Scale(springForceVector, dt));
443
444 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
445 Vector3Scale(level->placementTowerSpring.velocity, dt));
446 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
447 {
448 level->placementTowerSpring.velocity.y *= -1.0f;
449 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
450 }
451 }
452 }
453
454 void DrawLevelBuildingPlacementState(Level *level)
455 {
456 const float placementDuration = 0.5f;
457
458 level->placementTimer += gameTime.deltaTime;
459 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
460 {
461 level->placementPhase = PLACEMENT_PHASE_MOVING;
462 level->placementTimer = 0.0f;
463 }
464
465 BeginMode3D(level->camera);
466 DrawLevelGround(level);
467
468 int blockedCellCount = 0;
469 Vector2 blockedCells[1];
470 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
471 float planeDistance = ray.position.y / -ray.direction.y;
472 float planeX = ray.direction.x * planeDistance + ray.position.x;
473 float planeY = ray.direction.z * planeDistance + ray.position.z;
474 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
475 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
476 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
477 level->placementMode && !guiState.isBlocked &&
478 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
479 {
480 level->placementX = mapX;
481 level->placementY = mapY;
482 }
483 else
484 {
485 mapX = level->placementX;
486 mapY = level->placementY;
487 }
488 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
489 PathFindingMapUpdate(blockedCellCount, blockedCells);
490
491 TowerDraw();
492 EnemyDraw();
493 ProjectileDraw();
494 ParticleDraw();
495 DrawEnemyPaths(level);
496
497 // let the tower float up and down. Consider this height in the spring simulation as well
498 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
499
500 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
501 {
502 // The bouncing spring needs a bit of outro time to look nice and complete.
503 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
504 // and the last 1/3rd is the outro physics (bouncing)
505 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
506 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
507 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
508 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
509 towerFloatHeight = linearBlendHeight + parabola;
510 }
511
512 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
513
514 rlPushMatrix();
515 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
516
517 rlPushMatrix();
518 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
519 // calculate x and z rotation to align the model with the spring
520 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
521 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
522 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
523 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
524 float springLength = Vector3Length(towerUp);
525 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
526 float towerSquash = 1.0f / towerStretch;
527 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
528 rlScalef(towerSquash, towerStretch, towerSquash);
529 Tower dummy = {
530 .towerType = level->placementMode,
531 };
532 TowerDrawSingle(dummy);
533 rlPopMatrix();
534
535 // draw a shadow for the tower
536 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
537 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
538 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
539
540
541 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
542 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
543 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
544 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
545
546 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
547 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
548 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
549 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
550 rlPopMatrix();
551
552 guiState.isBlocked = 0;
553
554 EndMode3D();
555
556 TowerDrawHealthBars(level->camera);
557
558 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
559 {
560 if (level->placementTimer > placementDuration)
561 {
562 TowerTryAdd(level->placementMode, mapX, mapY);
563 level->playerGold -= GetTowerCosts(level->placementMode);
564 level->nextState = LEVEL_STATE_BUILDING;
565 level->placementMode = TOWER_TYPE_NONE;
566 }
567 }
568 else
569 {
570 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
571 {
572 level->nextState = LEVEL_STATE_BUILDING;
573 level->placementMode = TOWER_TYPE_NONE;
574 TraceLog(LOG_INFO, "Cancel building");
575 }
576
577 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
578 {
579 level->placementPhase = PLACEMENT_PHASE_PLACING;
580 level->placementTimer = 0.0f;
581 }
582 }
583 }
584
585 void DrawLevelBuildingState(Level *level)
586 {
587 BeginMode3D(level->camera);
588 DrawLevelGround(level);
589
590 PathFindingMapUpdate(0, 0);
591 TowerDraw();
592 EnemyDraw();
593 ProjectileDraw();
594 ParticleDraw();
595 DrawEnemyPaths(level);
596
597 guiState.isBlocked = 0;
598
599 EndMode3D();
600
601 TowerDrawHealthBars(level->camera);
602
603 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
604 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
605 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
606 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
607
608 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
609 {
610 level->nextState = LEVEL_STATE_RESET;
611 }
612
613 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
614 {
615 level->nextState = LEVEL_STATE_BATTLE;
616 }
617
618 const char *text = "Building phase";
619 int textWidth = MeasureText(text, 20);
620 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
621 }
622
623 void InitBattleStateConditions(Level *level)
624 {
625 level->state = LEVEL_STATE_BATTLE;
626 level->nextState = LEVEL_STATE_NONE;
627 level->waveEndTimer = 0.0f;
628 for (int i = 0; i < 10; i++)
629 {
630 EnemyWave *wave = &level->waves[i];
631 wave->spawned = 0;
632 wave->timeToSpawnNext = wave->delay;
633 }
634 }
635
636 void DrawLevelBattleState(Level *level)
637 {
638 BeginMode3D(level->camera);
639 DrawLevelGround(level);
640 TowerDraw();
641 EnemyDraw();
642 ProjectileDraw();
643 ParticleDraw();
644 guiState.isBlocked = 0;
645 EndMode3D();
646
647 EnemyDrawHealthbars(level->camera);
648 TowerDrawHealthBars(level->camera);
649
650 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
651 {
652 level->nextState = LEVEL_STATE_RESET;
653 }
654
655 int maxCount = 0;
656 int remainingCount = 0;
657 for (int i = 0; i < 10; i++)
658 {
659 EnemyWave *wave = &level->waves[i];
660 if (wave->wave != level->currentWave)
661 {
662 continue;
663 }
664 maxCount += wave->count;
665 remainingCount += wave->count - wave->spawned;
666 }
667 int aliveCount = EnemyCount();
668 remainingCount += aliveCount;
669
670 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
671 int textWidth = MeasureText(text, 20);
672 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
673 }
674
675 void DrawLevel(Level *level)
676 {
677 switch (level->state)
678 {
679 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
680 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
681 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
682 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
683 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
684 default: break;
685 }
686
687 DrawLevelHud(level);
688 }
689
690 void UpdateLevel(Level *level)
691 {
692 if (level->state == LEVEL_STATE_BATTLE)
693 {
694 int activeWaves = 0;
695 for (int i = 0; i < 10; i++)
696 {
697 EnemyWave *wave = &level->waves[i];
698 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
699 {
700 continue;
701 }
702 activeWaves++;
703 wave->timeToSpawnNext -= gameTime.deltaTime;
704 if (wave->timeToSpawnNext <= 0.0f)
705 {
706 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
707 if (enemy)
708 {
709 wave->timeToSpawnNext = wave->interval;
710 wave->spawned++;
711 }
712 }
713 }
714 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
715 level->waveEndTimer += gameTime.deltaTime;
716 if (level->waveEndTimer >= 2.0f)
717 {
718 level->nextState = LEVEL_STATE_LOST_WAVE;
719 }
720 }
721 else if (activeWaves == 0 && EnemyCount() == 0)
722 {
723 level->waveEndTimer += gameTime.deltaTime;
724 if (level->waveEndTimer >= 2.0f)
725 {
726 level->nextState = LEVEL_STATE_WON_WAVE;
727 }
728 }
729 }
730
731 PathFindingMapUpdate(0, 0);
732 EnemyUpdate();
733 TowerUpdate();
734 ProjectileUpdate();
735 ParticleUpdate();
736
737 if (level->nextState == LEVEL_STATE_RESET)
738 {
739 InitLevel(level);
740 }
741
742 if (level->nextState == LEVEL_STATE_BATTLE)
743 {
744 InitBattleStateConditions(level);
745 }
746
747 if (level->nextState == LEVEL_STATE_WON_WAVE)
748 {
749 level->currentWave++;
750 level->state = LEVEL_STATE_WON_WAVE;
751 }
752
753 if (level->nextState == LEVEL_STATE_LOST_WAVE)
754 {
755 level->state = LEVEL_STATE_LOST_WAVE;
756 }
757
758 if (level->nextState == LEVEL_STATE_BUILDING)
759 {
760 level->state = LEVEL_STATE_BUILDING;
761 }
762
763 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
764 {
765 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
766 level->placementTransitionPosition = (Vector2){
767 level->placementX, level->placementY};
768 // initialize the spring to the current position
769 level->placementTowerSpring = (PhysicsPoint){
770 .position = (Vector3){level->placementX, 8.0f, level->placementY},
771 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
772 };
773 level->placementPhase = PLACEMENT_PHASE_STARTING;
774 level->placementTimer = 0.0f;
775 }
776
777 if (level->nextState == LEVEL_STATE_WON_LEVEL)
778 {
779 // make something of this later
780 InitLevel(level);
781 }
782
783 level->nextState = LEVEL_STATE_NONE;
784 }
785
786 float nextSpawnTime = 0.0f;
787
788 void ResetGame()
789 {
790 InitLevel(currentLevel);
791 }
792
793 void InitGame()
794 {
795 TowerInit();
796 EnemyInit();
797 ProjectileInit();
798 ParticleInit();
799 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
800
801 currentLevel = levels;
802 InitLevel(currentLevel);
803 }
804
805 //# Immediate GUI functions
806
807 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
808 {
809 const float healthBarHeight = 6.0f;
810 const float healthBarOffset = 15.0f;
811 const float inset = 2.0f;
812 const float innerWidth = healthBarWidth - inset * 2;
813 const float innerHeight = healthBarHeight - inset * 2;
814
815 Vector2 screenPos = GetWorldToScreen(position, camera);
816 float centerX = screenPos.x - healthBarWidth * 0.5f;
817 float topY = screenPos.y - healthBarOffset;
818 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
819 float healthWidth = innerWidth * healthRatio;
820 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
821 }
822
823 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
824 {
825 Rectangle bounds = {x, y, width, height};
826 int isPressed = 0;
827 int isSelected = state && state->isSelected;
828 int isDisabled = state && state->isDisabled;
829 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
830 {
831 Color color = isSelected ? DARKGRAY : GRAY;
832 DrawRectangle(x, y, width, height, color);
833 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
834 {
835 isPressed = 1;
836 }
837 guiState.isBlocked = 1;
838 }
839 else
840 {
841 Color color = isSelected ? WHITE : LIGHTGRAY;
842 DrawRectangle(x, y, width, height, color);
843 }
844 Font font = GetFontDefault();
845 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
846 Color textColor = isDisabled ? GRAY : BLACK;
847 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
848 return isPressed;
849 }
850
851 //# Main game loop
852
853 void GameUpdate()
854 {
855 UpdateLevel(currentLevel);
856 }
857
858 int main(void)
859 {
860 int screenWidth, screenHeight;
861 GetPreferredSize(&screenWidth, &screenHeight);
862 InitWindow(screenWidth, screenHeight, "Tower defense");
863 float gamespeed = 1.0f;
864 SetTargetFPS(30);
865
866 LoadAssets();
867 InitGame();
868
869 float pause = 1.0f;
870
871 while (!WindowShouldClose())
872 {
873 if (IsPaused()) {
874 // canvas is not visible in browser - do nothing
875 continue;
876 }
877
878 if (IsKeyPressed(KEY_T))
879 {
880 gamespeed += 0.1f;
881 if (gamespeed > 1.05f) gamespeed = 0.1f;
882 }
883
884 if (IsKeyPressed(KEY_P))
885 {
886 pause = pause > 0.5f ? 0.0f : 1.0f;
887 }
888
889 float dt = GetFrameTime() * gamespeed * pause;
890 // cap maximum delta time to 0.1 seconds to prevent large time steps
891 if (dt > 0.1f) dt = 0.1f;
892 gameTime.time += dt;
893 gameTime.deltaTime = dt;
894 gameTime.frameCount += 1;
895
896 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
897 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
898
899 BeginDrawing();
900 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
901
902 GameUpdate();
903 DrawLevel(currentLevel);
904
905 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
906 EndDrawing();
907
908 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
909 }
910
911 CloseWindow();
912
913 return 0;
914 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 typedef struct PhysicsPoint
12 {
13 Vector3 position;
14 Vector3 velocity;
15 } PhysicsPoint;
16
17 #define ENEMY_MAX_PATH_COUNT 8
18 #define ENEMY_MAX_COUNT 400
19 #define ENEMY_TYPE_NONE 0
20
21 #define ENEMY_TYPE_MINION 1
22 #define ENEMY_TYPE_RUNNER 2
23 #define ENEMY_TYPE_SHIELD 3
24 #define ENEMY_TYPE_BOSS 4
25
26 #define PARTICLE_MAX_COUNT 400
27 #define PARTICLE_TYPE_NONE 0
28 #define PARTICLE_TYPE_EXPLOSION 1
29
30 typedef struct Particle
31 {
32 uint8_t particleType;
33 float spawnTime;
34 float lifetime;
35 Vector3 position;
36 Vector3 velocity;
37 Vector3 scale;
38 } Particle;
39
40 #define TOWER_MAX_COUNT 400
41 enum TowerType
42 {
43 TOWER_TYPE_NONE,
44 TOWER_TYPE_BASE,
45 TOWER_TYPE_ARCHER,
46 TOWER_TYPE_BALLISTA,
47 TOWER_TYPE_CATAPULT,
48 TOWER_TYPE_WALL,
49 TOWER_TYPE_COUNT
50 };
51
52 typedef struct HitEffectConfig
53 {
54 float damage;
55 float areaDamageRadius;
56 float pushbackPowerDistance;
57 } HitEffectConfig;
58
59 typedef struct TowerTypeConfig
60 {
61 float cooldown;
62 float range;
63 float projectileSpeed;
64
65 uint8_t cost;
66 uint8_t projectileType;
67 uint16_t maxHealth;
68
69 HitEffectConfig hitEffect;
70 } TowerTypeConfig;
71
72 typedef struct Tower
73 {
74 int16_t x, y;
75 uint8_t towerType;
76 Vector2 lastTargetPosition;
77 float cooldown;
78 float damage;
79 } Tower;
80
81 typedef struct GameTime
82 {
83 float time;
84 float deltaTime;
85 uint32_t frameCount;
86
87 float fixedDeltaTime;
88 // leaving the fixed time stepping to the update functions,
89 // we need to know the fixed time at the start of the frame
90 float fixedTimeStart;
91 // and the number of fixed steps that we have to make this frame
92 // The fixedTime is fixedTimeStart + n * fixedStepCount
93 uint8_t fixedStepCount;
94 } GameTime;
95
96 typedef struct ButtonState {
97 char isSelected;
98 char isDisabled;
99 } ButtonState;
100
101 typedef struct GUIState {
102 int isBlocked;
103 } GUIState;
104
105 typedef enum LevelState
106 {
107 LEVEL_STATE_NONE,
108 LEVEL_STATE_BUILDING,
109 LEVEL_STATE_BUILDING_PLACEMENT,
110 LEVEL_STATE_BATTLE,
111 LEVEL_STATE_WON_WAVE,
112 LEVEL_STATE_LOST_WAVE,
113 LEVEL_STATE_WON_LEVEL,
114 LEVEL_STATE_RESET,
115 } LevelState;
116
117 typedef struct EnemyWave {
118 uint8_t enemyType;
119 uint8_t wave;
120 uint16_t count;
121 float interval;
122 float delay;
123 Vector2 spawnPosition;
124
125 uint16_t spawned;
126 float timeToSpawnNext;
127 } EnemyWave;
128
129 #define ENEMY_MAX_WAVE_COUNT 10
130
131 typedef enum PlacementPhase
132 {
133 PLACEMENT_PHASE_STARTING,
134 PLACEMENT_PHASE_MOVING,
135 PLACEMENT_PHASE_PLACING,
136 } PlacementPhase;
137
138 typedef struct Level
139 {
140 int seed;
141 LevelState state;
142 LevelState nextState;
143 Camera3D camera;
144 int placementMode;
145 PlacementPhase placementPhase;
146 float placementTimer;
147 int16_t placementX;
148 int16_t placementY;
149 Vector2 placementTransitionPosition;
150 PhysicsPoint placementTowerSpring;
151
152 int initialGold;
153 int playerGold;
154
155 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
156 int currentWave;
157 float waveEndTimer;
158 } Level;
159
160 typedef struct DeltaSrc
161 {
162 char x, y;
163 } DeltaSrc;
164
165 typedef struct PathfindingMap
166 {
167 int width, height;
168 float scale;
169 float *distances;
170 long *towerIndex;
171 DeltaSrc *deltaSrc;
172 float maxDistance;
173 Matrix toMapSpace;
174 Matrix toWorldSpace;
175 } PathfindingMap;
176
177 // when we execute the pathfinding algorithm, we need to store the active nodes
178 // in a queue. Each node has a position, a distance from the start, and the
179 // position of the node that we came from.
180 typedef struct PathfindingNode
181 {
182 int16_t x, y, fromX, fromY;
183 float distance;
184 } PathfindingNode;
185
186 typedef struct EnemyId
187 {
188 uint16_t index;
189 uint16_t generation;
190 } EnemyId;
191
192 typedef struct EnemyClassConfig
193 {
194 float speed;
195 float health;
196 float radius;
197 float maxAcceleration;
198 float requiredContactTime;
199 float explosionDamage;
200 float explosionRange;
201 float explosionPushbackPower;
202 int goldValue;
203 } EnemyClassConfig;
204
205 typedef struct Enemy
206 {
207 int16_t currentX, currentY;
208 int16_t nextX, nextY;
209 Vector2 simPosition;
210 Vector2 simVelocity;
211 uint16_t generation;
212 float walkedDistance;
213 float startMovingTime;
214 float damage, futureDamage;
215 float contactTime;
216 uint8_t enemyType;
217 uint8_t movePathCount;
218 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
219 } Enemy;
220
221 // a unit that uses sprites to be drawn
222 #define SPRITE_UNIT_ANIMATION_COUNT 6
223 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
224 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
225
226 typedef struct SpriteAnimation
227 {
228 Rectangle srcRect;
229 Vector2 offset;
230 uint8_t animationId;
231 uint8_t frameCount;
232 uint8_t frameWidth;
233 float frameDuration;
234 } SpriteAnimation;
235
236 typedef struct SpriteUnit
237 {
238 float scale;
239 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
240 } SpriteUnit;
241
242 #define PROJECTILE_MAX_COUNT 1200
243 #define PROJECTILE_TYPE_NONE 0
244 #define PROJECTILE_TYPE_ARROW 1
245 #define PROJECTILE_TYPE_CATAPULT 2
246 #define PROJECTILE_TYPE_BALLISTA 3
247
248 typedef struct Projectile
249 {
250 uint8_t projectileType;
251 float shootTime;
252 float arrivalTime;
253 float distance;
254 Vector3 position;
255 Vector3 target;
256 Vector3 directionNormal;
257 EnemyId targetEnemy;
258 HitEffectConfig hitEffectConfig;
259 } Projectile;
260
261 //# Function declarations
262 float TowerGetMaxHealth(Tower *tower);
263 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
264 int EnemyAddDamageRange(Vector2 position, float range, float damage);
265 int EnemyAddDamage(Enemy *enemy, float damage);
266
267 //# Enemy functions
268 void EnemyInit();
269 void EnemyDraw();
270 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
271 void EnemyUpdate();
272 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
273 float EnemyGetMaxHealth(Enemy *enemy);
274 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
275 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
276 EnemyId EnemyGetId(Enemy *enemy);
277 Enemy *EnemyTryResolve(EnemyId enemyId);
278 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
279 int EnemyAddDamage(Enemy *enemy, float damage);
280 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
281 int EnemyCount();
282 void EnemyDrawHealthbars(Camera3D camera);
283
284 //# Tower functions
285 void TowerInit();
286 Tower *TowerGetAt(int16_t x, int16_t y);
287 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
288 Tower *GetTowerByType(uint8_t towerType);
289 int GetTowerCosts(uint8_t towerType);
290 float TowerGetMaxHealth(Tower *tower);
291 void TowerDraw();
292 void TowerDrawSingle(Tower tower);
293 void TowerUpdate();
294 void TowerDrawHealthBars(Camera3D camera);
295 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
296
297 //# Particles
298 void ParticleInit();
299 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
300 void ParticleUpdate();
301 void ParticleDraw();
302
303 //# Projectiles
304 void ProjectileInit();
305 void ProjectileDraw();
306 void ProjectileUpdate();
307 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
308
309 //# Pathfinding map
310 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
311 float PathFindingGetDistance(int mapX, int mapY);
312 Vector2 PathFindingGetGradient(Vector3 world);
313 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
314 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
315 void PathFindingMapDraw();
316
317 //# UI
318 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
319
320 //# Level
321 void DrawLevelGround(Level *level);
322 void DrawEnemyPath(Level *level, Color arrowColor);
323
324 //# variables
325 extern Level *currentLevel;
326 extern Enemy enemies[ENEMY_MAX_COUNT];
327 extern int enemyCount;
328 extern EnemyClassConfig enemyClassConfigs[];
329
330 extern GUIState guiState;
331 extern GameTime gameTime;
332 extern Tower towers[TOWER_MAX_COUNT];
333 extern int towerCount;
334
335 extern Texture2D palette, spriteSheet;
336
337 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .animations[0] = {
57 .srcRect = {0, 0, 16, 16},
58 .offset = {7, 1},
59 .frameCount = 1,
60 .frameDuration = 0.0f,
61 },
62 .animations[1] = {
63 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
64 .srcRect = {16, 0, 6, 16},
65 .offset = {8, 0},
66 },
67 .animations[2] = {
68 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
69 .srcRect = {22, 0, 11, 16},
70 .offset = {10, 0},
71 },
72 };
73
74 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
75 {
76 float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
77 float xScale = flip ? -1.0f : 1.0f;
78 Camera3D camera = currentLevel->camera;
79 float size = 0.5f * unitScale;
80 // we want the sprite to face the camera, so we need to calculate the up vector
81 Vector3 forward = Vector3Subtract(camera.target, camera.position);
82 Vector3 up = {0, 1, 0};
83 Vector3 right = Vector3CrossProduct(forward, up);
84 up = Vector3Normalize(Vector3CrossProduct(right, forward));
85
86 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
87 {
88 SpriteAnimation anim = unit.animations[i];
89 if (anim.animationId != phase && anim.animationId != 0)
90 {
91 continue;
92 }
93 Rectangle srcRect = anim.srcRect;
94 if (anim.frameCount > 1)
95 {
96 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
97 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
98 }
99 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
100 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
101
102 if (flip)
103 {
104 srcRect.x += srcRect.width;
105 srcRect.width = -srcRect.width;
106 offset.x = scale.x - offset.x;
107 }
108 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
109 // move the sprite slightly towards the camera to avoid z-fighting
110 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
111 }
112 }
113
114 void TowerInit()
115 {
116 for (int i = 0; i < TOWER_MAX_COUNT; i++)
117 {
118 towers[i] = (Tower){0};
119 }
120 towerCount = 0;
121
122 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
123 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
124
125 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
126 {
127 if (towerModels[i].materials)
128 {
129 // assign the palette texture to the material of the model (0 is not used afaik)
130 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
131 }
132 }
133 }
134
135 static void TowerGunUpdate(Tower *tower)
136 {
137 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
138 if (tower->cooldown <= 0.0f)
139 {
140 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
141 if (enemy)
142 {
143 tower->cooldown = config.cooldown;
144 // shoot the enemy; determine future position of the enemy
145 float bulletSpeed = config.projectileSpeed;
146 Vector2 velocity = enemy->simVelocity;
147 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
148 Vector2 towerPosition = {tower->x, tower->y};
149 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
150 for (int i = 0; i < 8; i++) {
151 velocity = enemy->simVelocity;
152 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
153 float distance = Vector2Distance(towerPosition, futurePosition);
154 float eta2 = distance / bulletSpeed;
155 if (fabs(eta - eta2) < 0.01f) {
156 break;
157 }
158 eta = (eta2 + eta) * 0.5f;
159 }
160
161 ProjectileTryAdd(config.projectileType, enemy,
162 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
163 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
164 bulletSpeed, config.hitEffect);
165 enemy->futureDamage += config.hitEffect.damage;
166 tower->lastTargetPosition = futurePosition;
167 }
168 }
169 else
170 {
171 tower->cooldown -= gameTime.deltaTime;
172 }
173 }
174
175 Tower *TowerGetAt(int16_t x, int16_t y)
176 {
177 for (int i = 0; i < towerCount; i++)
178 {
179 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
180 {
181 return &towers[i];
182 }
183 }
184 return 0;
185 }
186
187 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
188 {
189 if (towerCount >= TOWER_MAX_COUNT)
190 {
191 return 0;
192 }
193
194 Tower *tower = TowerGetAt(x, y);
195 if (tower)
196 {
197 return 0;
198 }
199
200 tower = &towers[towerCount++];
201 tower->x = x;
202 tower->y = y;
203 tower->towerType = towerType;
204 tower->cooldown = 0.0f;
205 tower->damage = 0.0f;
206 return tower;
207 }
208
209 Tower *GetTowerByType(uint8_t towerType)
210 {
211 for (int i = 0; i < towerCount; i++)
212 {
213 if (towers[i].towerType == towerType)
214 {
215 return &towers[i];
216 }
217 }
218 return 0;
219 }
220
221 int GetTowerCosts(uint8_t towerType)
222 {
223 return towerTypeConfigs[towerType].cost;
224 }
225
226 float TowerGetMaxHealth(Tower *tower)
227 {
228 return towerTypeConfigs[tower->towerType].maxHealth;
229 }
230
231 void TowerDrawSingle(Tower tower)
232 {
233 if (tower.towerType == TOWER_TYPE_NONE)
234 {
235 return;
236 }
237
238 switch (tower.towerType)
239 {
240 case TOWER_TYPE_ARCHER:
241 {
242 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
243 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
244 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
245 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
246 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
247 }
248 break;
249 case TOWER_TYPE_BALLISTA:
250 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
251 break;
252 case TOWER_TYPE_CATAPULT:
253 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
254 break;
255 default:
256 if (towerModels[tower.towerType].materials)
257 {
258 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
259 } else {
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
261 }
262 break;
263 }
264 }
265
266 void TowerDraw()
267 {
268 for (int i = 0; i < towerCount; i++)
269 {
270 TowerDrawSingle(towers[i]);
271 }
272 }
273
274 void TowerUpdate()
275 {
276 for (int i = 0; i < towerCount; i++)
277 {
278 Tower *tower = &towers[i];
279 switch (tower->towerType)
280 {
281 case TOWER_TYPE_CATAPULT:
282 case TOWER_TYPE_BALLISTA:
283 case TOWER_TYPE_ARCHER:
284 TowerGunUpdate(tower);
285 break;
286 }
287 }
288 }
289
290 void TowerDrawHealthBars(Camera3D camera)
291 {
292 for (int i = 0; i < towerCount; i++)
293 {
294 Tower *tower = &towers[i];
295 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
296 {
297 continue;
298 }
299
300 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
301 float maxHealth = TowerGetMaxHealth(tower);
302 float health = maxHealth - tower->damage;
303 float healthRatio = health / maxHealth;
304
305 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
306 }
307 }
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 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 20.0f,
32 .speed = 0.5f,
33 .radius = 0.25f,
34 .maxAcceleration = 1.0f,
35 .explosionDamage = 2.0f,
36 .requiredContactTime = 0.5f,
37 .explosionRange = 1.0f,
38 .explosionPushbackPower = 0.25f,
39 .goldValue = 3,
40 },
41 [ENEMY_TYPE_BOSS] = {
42 .health = 50.0f,
43 .speed = 0.4f,
44 .radius = 0.25f,
45 .maxAcceleration = 1.0f,
46 .explosionDamage = 5.0f,
47 .requiredContactTime = 0.5f,
48 .explosionRange = 1.0f,
49 .explosionPushbackPower = 0.25f,
50 .goldValue = 10,
51 },
52 };
53
54 Enemy enemies[ENEMY_MAX_COUNT];
55 int enemyCount = 0;
56
57 SpriteUnit enemySprites[] = {
58 [ENEMY_TYPE_MINION] = {
59 .animations[0] = {
60 .srcRect = {0, 17, 16, 15},
61 .offset = {8.0f, 0.0f},
62 .frameCount = 6,
63 .frameDuration = 0.1f,
64 },
65 .animations[1] = {
66 .srcRect = {1, 33, 15, 14},
67 .offset = {7.0f, 0.0f},
68 .frameCount = 6,
69 .frameWidth = 16,
70 .frameDuration = 0.1f,
71 },
72 },
73 [ENEMY_TYPE_RUNNER] = {
74 .scale = 0.75f,
75 .animations[0] = {
76 .srcRect = {0, 17, 16, 15},
77 .offset = {8.0f, 0.0f},
78 .frameCount = 6,
79 .frameDuration = 0.1f,
80 },
81 },
82 [ENEMY_TYPE_SHIELD] = {
83 .animations[0] = {
84 .srcRect = {0, 17, 16, 15},
85 .offset = {8.0f, 0.0f},
86 .frameCount = 6,
87 .frameDuration = 0.1f,
88 },
89 .animations[1] = {
90 .srcRect = {99, 17, 10, 11},
91 .offset = {7.0f, 0.0f},
92 },
93 },
94 [ENEMY_TYPE_BOSS] = {
95 .scale = 1.5f,
96 .animations[0] = {
97 .srcRect = {0, 17, 16, 15},
98 .offset = {8.0f, 0.0f},
99 .frameCount = 6,
100 .frameDuration = 0.1f,
101 },
102 .animations[1] = {
103 .srcRect = {97, 29, 14, 7},
104 .offset = {7.0f, -9.0f},
105 },
106 },
107 };
108
109 void EnemyInit()
110 {
111 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
112 {
113 enemies[i] = (Enemy){0};
114 }
115 enemyCount = 0;
116 }
117
118 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
119 {
120 return enemyClassConfigs[enemy->enemyType].speed;
121 }
122
123 float EnemyGetMaxHealth(Enemy *enemy)
124 {
125 return enemyClassConfigs[enemy->enemyType].health;
126 }
127
128 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
129 {
130 int16_t castleX = 0;
131 int16_t castleY = 0;
132 int16_t dx = castleX - currentX;
133 int16_t dy = castleY - currentY;
134 if (dx == 0 && dy == 0)
135 {
136 *nextX = currentX;
137 *nextY = currentY;
138 return 1;
139 }
140 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
141
142 if (gradient.x == 0 && gradient.y == 0)
143 {
144 *nextX = currentX;
145 *nextY = currentY;
146 return 1;
147 }
148
149 if (fabsf(gradient.x) > fabsf(gradient.y))
150 {
151 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
152 *nextY = currentY;
153 return 0;
154 }
155 *nextX = currentX;
156 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
157 return 0;
158 }
159
160
161 // this function predicts the movement of the unit for the next deltaT seconds
162 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
163 {
164 const float pointReachedDistance = 0.25f;
165 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
166 const float maxSimStepTime = 0.015625f;
167
168 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
169 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
170 int16_t nextX = enemy->nextX;
171 int16_t nextY = enemy->nextY;
172 Vector2 position = enemy->simPosition;
173 int passedCount = 0;
174 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
175 {
176 float stepTime = fminf(deltaT - t, maxSimStepTime);
177 Vector2 target = (Vector2){nextX, nextY};
178 float speed = Vector2Length(*velocity);
179 // draw the target position for debugging
180 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
181 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
182 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
183 {
184 // we reached the target position, let's move to the next waypoint
185 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
186 target = (Vector2){nextX, nextY};
187 // track how many waypoints we passed
188 passedCount++;
189 }
190
191 // acceleration towards the target
192 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
193 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
194 *velocity = Vector2Add(*velocity, acceleration);
195
196 // limit the speed to the maximum speed
197 if (speed > maxSpeed)
198 {
199 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
200 }
201
202 // move the enemy
203 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
204 }
205
206 if (waypointPassedCount)
207 {
208 (*waypointPassedCount) = passedCount;
209 }
210
211 return position;
212 }
213
214 void EnemyDraw()
215 {
216 rlDrawRenderBatchActive();
217 rlDisableDepthMask();
218 for (int i = 0; i < enemyCount; i++)
219 {
220 Enemy enemy = enemies[i];
221 if (enemy.enemyType == ENEMY_TYPE_NONE)
222 {
223 continue;
224 }
225
226 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
227
228 // don't draw any trails for now; might replace this with footprints later
229 // if (enemy.movePathCount > 0)
230 // {
231 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
232 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
233 // }
234 // for (int j = 1; j < enemy.movePathCount; j++)
235 // {
236 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
237 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
238 // DrawLine3D(p, q, GREEN);
239 // }
240
241 switch (enemy.enemyType)
242 {
243 case ENEMY_TYPE_MINION:
244 case ENEMY_TYPE_RUNNER:
245 case ENEMY_TYPE_SHIELD:
246 case ENEMY_TYPE_BOSS:
247 DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y},
248 enemy.walkedDistance, 0, 0);
249 break;
250 }
251 }
252 rlDrawRenderBatchActive();
253 rlEnableDepthMask();
254 }
255
256 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
257 {
258 // damage the tower
259 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
260 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
261 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
262 float explosionRange2 = explosionRange * explosionRange;
263 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
264 // explode the enemy
265 if (tower->damage >= TowerGetMaxHealth(tower))
266 {
267 tower->towerType = TOWER_TYPE_NONE;
268 }
269
270 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
271 explosionSource,
272 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
273
274 enemy->enemyType = ENEMY_TYPE_NONE;
275
276 // push back enemies & dealing damage
277 for (int i = 0; i < enemyCount; i++)
278 {
279 Enemy *other = &enemies[i];
280 if (other->enemyType == ENEMY_TYPE_NONE)
281 {
282 continue;
283 }
284 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
285 if (distanceSqr > 0 && distanceSqr < explosionRange2)
286 {
287 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
288 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
289 EnemyAddDamage(other, explosionDamge);
290 }
291 }
292 }
293
294 void EnemyUpdate()
295 {
296 const float castleX = 0;
297 const float castleY = 0;
298 const float maxPathDistance2 = 0.25f * 0.25f;
299
300 for (int i = 0; i < enemyCount; i++)
301 {
302 Enemy *enemy = &enemies[i];
303 if (enemy->enemyType == ENEMY_TYPE_NONE)
304 {
305 continue;
306 }
307
308 int waypointPassedCount = 0;
309 Vector2 prevPosition = enemy->simPosition;
310 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
311 enemy->startMovingTime = gameTime.time;
312 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
313 // track path of unit
314 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
315 {
316 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
317 {
318 enemy->movePath[j] = enemy->movePath[j - 1];
319 }
320 enemy->movePath[0] = enemy->simPosition;
321 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
322 {
323 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
324 }
325 }
326
327 if (waypointPassedCount > 0)
328 {
329 enemy->currentX = enemy->nextX;
330 enemy->currentY = enemy->nextY;
331 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
332 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
333 {
334 // enemy reached the castle; remove it
335 enemy->enemyType = ENEMY_TYPE_NONE;
336 continue;
337 }
338 }
339 }
340
341 // handle collisions between enemies
342 for (int i = 0; i < enemyCount - 1; i++)
343 {
344 Enemy *enemyA = &enemies[i];
345 if (enemyA->enemyType == ENEMY_TYPE_NONE)
346 {
347 continue;
348 }
349 for (int j = i + 1; j < enemyCount; j++)
350 {
351 Enemy *enemyB = &enemies[j];
352 if (enemyB->enemyType == ENEMY_TYPE_NONE)
353 {
354 continue;
355 }
356 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
357 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
358 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
359 float radiusSum = radiusA + radiusB;
360 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
361 {
362 // collision
363 float distance = sqrtf(distanceSqr);
364 float overlap = radiusSum - distance;
365 // move the enemies apart, but softly; if we have a clog of enemies,
366 // moving them perfectly apart can cause them to jitter
367 float positionCorrection = overlap / 5.0f;
368 Vector2 direction = (Vector2){
369 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
370 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
371 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
372 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
373 }
374 }
375 }
376
377 // handle collisions between enemies and towers
378 for (int i = 0; i < enemyCount; i++)
379 {
380 Enemy *enemy = &enemies[i];
381 if (enemy->enemyType == ENEMY_TYPE_NONE)
382 {
383 continue;
384 }
385 enemy->contactTime -= gameTime.deltaTime;
386 if (enemy->contactTime < 0.0f)
387 {
388 enemy->contactTime = 0.0f;
389 }
390
391 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
392 // linear search over towers; could be optimized by using path finding tower map,
393 // but for now, we keep it simple
394 for (int j = 0; j < towerCount; j++)
395 {
396 Tower *tower = &towers[j];
397 if (tower->towerType == TOWER_TYPE_NONE)
398 {
399 continue;
400 }
401 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
402 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
403 if (distanceSqr > combinedRadius * combinedRadius)
404 {
405 continue;
406 }
407 // potential collision; square / circle intersection
408 float dx = tower->x - enemy->simPosition.x;
409 float dy = tower->y - enemy->simPosition.y;
410 float absDx = fabsf(dx);
411 float absDy = fabsf(dy);
412 Vector3 contactPoint = {0};
413 if (absDx <= 0.5f && absDx <= absDy) {
414 // vertical collision; push the enemy out horizontally
415 float overlap = enemyRadius + 0.5f - absDy;
416 if (overlap < 0.0f)
417 {
418 continue;
419 }
420 float direction = dy > 0.0f ? -1.0f : 1.0f;
421 enemy->simPosition.y += direction * overlap;
422 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
423 }
424 else if (absDy <= 0.5f && absDy <= absDx)
425 {
426 // horizontal collision; push the enemy out vertically
427 float overlap = enemyRadius + 0.5f - absDx;
428 if (overlap < 0.0f)
429 {
430 continue;
431 }
432 float direction = dx > 0.0f ? -1.0f : 1.0f;
433 enemy->simPosition.x += direction * overlap;
434 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
435 }
436 else
437 {
438 // possible collision with a corner
439 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
440 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
441 float cornerX = tower->x + cornerDX;
442 float cornerY = tower->y + cornerDY;
443 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
444 if (cornerDistanceSqr > enemyRadius * enemyRadius)
445 {
446 continue;
447 }
448 // push the enemy out along the diagonal
449 float cornerDistance = sqrtf(cornerDistanceSqr);
450 float overlap = enemyRadius - cornerDistance;
451 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
452 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
453 enemy->simPosition.x -= directionX * overlap;
454 enemy->simPosition.y -= directionY * overlap;
455 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
456 }
457
458 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
459 {
460 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
461 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
462 {
463 EnemyTriggerExplode(enemy, tower, contactPoint);
464 }
465 }
466 }
467 }
468 }
469
470 EnemyId EnemyGetId(Enemy *enemy)
471 {
472 return (EnemyId){enemy - enemies, enemy->generation};
473 }
474
475 Enemy *EnemyTryResolve(EnemyId enemyId)
476 {
477 if (enemyId.index >= ENEMY_MAX_COUNT)
478 {
479 return 0;
480 }
481 Enemy *enemy = &enemies[enemyId.index];
482 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
483 {
484 return 0;
485 }
486 return enemy;
487 }
488
489 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
490 {
491 Enemy *spawn = 0;
492 for (int i = 0; i < enemyCount; i++)
493 {
494 Enemy *enemy = &enemies[i];
495 if (enemy->enemyType == ENEMY_TYPE_NONE)
496 {
497 spawn = enemy;
498 break;
499 }
500 }
501
502 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
503 {
504 spawn = &enemies[enemyCount++];
505 }
506
507 if (spawn)
508 {
509 spawn->currentX = currentX;
510 spawn->currentY = currentY;
511 spawn->nextX = currentX;
512 spawn->nextY = currentY;
513 spawn->simPosition = (Vector2){currentX, currentY};
514 spawn->simVelocity = (Vector2){0, 0};
515 spawn->enemyType = enemyType;
516 spawn->startMovingTime = gameTime.time;
517 spawn->damage = 0.0f;
518 spawn->futureDamage = 0.0f;
519 spawn->generation++;
520 spawn->movePathCount = 0;
521 spawn->walkedDistance = 0.0f;
522 }
523
524 return spawn;
525 }
526
527 int EnemyAddDamageRange(Vector2 position, float range, float damage)
528 {
529 int count = 0;
530 float range2 = range * range;
531 for (int i = 0; i < enemyCount; i++)
532 {
533 Enemy *enemy = &enemies[i];
534 if (enemy->enemyType == ENEMY_TYPE_NONE)
535 {
536 continue;
537 }
538 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
539 if (distance2 <= range2)
540 {
541 EnemyAddDamage(enemy, damage);
542 count++;
543 }
544 }
545 return count;
546 }
547
548 int EnemyAddDamage(Enemy *enemy, float damage)
549 {
550 enemy->damage += damage;
551 if (enemy->damage >= EnemyGetMaxHealth(enemy))
552 {
553 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
554 enemy->enemyType = ENEMY_TYPE_NONE;
555 return 1;
556 }
557
558 return 0;
559 }
560
561 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
562 {
563 int16_t castleX = 0;
564 int16_t castleY = 0;
565 Enemy* closest = 0;
566 int16_t closestDistance = 0;
567 float range2 = range * range;
568 for (int i = 0; i < enemyCount; i++)
569 {
570 Enemy* enemy = &enemies[i];
571 if (enemy->enemyType == ENEMY_TYPE_NONE)
572 {
573 continue;
574 }
575 float maxHealth = EnemyGetMaxHealth(enemy);
576 if (enemy->futureDamage >= maxHealth)
577 {
578 // ignore enemies that will die soon
579 continue;
580 }
581 int16_t dx = castleX - enemy->currentX;
582 int16_t dy = castleY - enemy->currentY;
583 int16_t distance = abs(dx) + abs(dy);
584 if (!closest || distance < closestDistance)
585 {
586 float tdx = towerX - enemy->currentX;
587 float tdy = towerY - enemy->currentY;
588 float tdistance2 = tdx * tdx + tdy * tdy;
589 if (tdistance2 <= range2)
590 {
591 closest = enemy;
592 closestDistance = distance;
593 }
594 }
595 }
596 return closest;
597 }
598
599 int EnemyCount()
600 {
601 int count = 0;
602 for (int i = 0; i < enemyCount; i++)
603 {
604 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
605 {
606 count++;
607 }
608 }
609 return count;
610 }
611
612 void EnemyDrawHealthbars(Camera3D camera)
613 {
614 for (int i = 0; i < enemyCount; i++)
615 {
616 Enemy *enemy = &enemies[i];
617 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
618 {
619 continue;
620 }
621 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
622 float maxHealth = EnemyGetMaxHealth(enemy);
623 float health = maxHealth - enemy->damage;
624 float healthRatio = health / maxHealth;
625
626 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
627 }
628 }
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 different enemies are now spawning in the game in the 1st and 2nd wave for testing purposes. A new scaling factor allows us to make the enemies bigger or smaller:
The graphics are now good enough for current state. The next rule to implement is to give the shield its own hitpoints and apply a damage reduction. The idea is that the the ballista's high damage is piercing the shield and the minion behind it while arrow attacks only damage the shield. When the shield is destroyed, the shield should now longer be drawn.
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {
10 .fixedDeltaTime = 1.0f / 60.0f,
11 };
12
13 Model floorTileAModel = {0};
14 Model floorTileBModel = {0};
15 Model treeModel[2] = {0};
16 Model firTreeModel[2] = {0};
17 Model rockModels[5] = {0};
18 Model grassPatchModel[1] = {0};
19
20 Model pathArrowModel = {0};
21 Model greenArrowModel = {0};
22
23 Texture2D palette, spriteSheet;
24
25 Level levels[] = {
26 [0] = {
27 .state = LEVEL_STATE_BUILDING,
28 .initialGold = 20,
29 .waves[0] = {
30 .enemyType = ENEMY_TYPE_SHIELD,
31 .wave = 0,
32 .count = 5,
33 .interval = 2.5f,
34 .delay = 1.0f,
35 .spawnPosition = {2, 6},
36 },
37 .waves[1] = {
38 .enemyType = ENEMY_TYPE_RUNNER,
39 .wave = 0,
40 .count = 5,
41 .interval = 2.5f,
42 .delay = 1.0f,
43 .spawnPosition = {-2, 6},
44 },
45 .waves[2] = {
46 .enemyType = ENEMY_TYPE_SHIELD,
47 .wave = 1,
48 .count = 20,
49 .interval = 1.5f,
50 .delay = 1.0f,
51 .spawnPosition = {0, 6},
52 },
53 .waves[3] = {
54 .enemyType = ENEMY_TYPE_MINION,
55 .wave = 2,
56 .count = 30,
57 .interval = 1.2f,
58 .delay = 1.0f,
59 .spawnPosition = {2, 6},
60 },
61 .waves[4] = {
62 .enemyType = ENEMY_TYPE_BOSS,
63 .wave = 2,
64 .count = 2,
65 .interval = 5.0f,
66 .delay = 2.0f,
67 .spawnPosition = {-2, 4},
68 }
69 },
70 };
71
72 Level *currentLevel = levels;
73
74 //# Game
75
76 static Model LoadGLBModel(char *filename)
77 {
78 Model model = LoadModel(TextFormat("data/%s.glb",filename));
79 for (int i = 0; i < model.materialCount; i++)
80 {
81 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
82 }
83 return model;
84 }
85
86 void LoadAssets()
87 {
88 // load a sprite sheet that contains all units
89 spriteSheet = LoadTexture("data/spritesheet.png");
90 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
91
92 // we'll use a palette texture to colorize the all buildings and environment art
93 palette = LoadTexture("data/palette.png");
94 // The texture uses gradients on very small space, so we'll enable bilinear filtering
95 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
96
97 floorTileAModel = LoadGLBModel("floor-tile-a");
98 floorTileBModel = LoadGLBModel("floor-tile-b");
99 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
100 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
101 firTreeModel[0] = LoadGLBModel("firtree-1-a");
102 firTreeModel[1] = LoadGLBModel("firtree-1-b");
103 rockModels[0] = LoadGLBModel("rock-1");
104 rockModels[1] = LoadGLBModel("rock-2");
105 rockModels[2] = LoadGLBModel("rock-3");
106 rockModels[3] = LoadGLBModel("rock-4");
107 rockModels[4] = LoadGLBModel("rock-5");
108 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
109
110 pathArrowModel = LoadGLBModel("direction-arrow-x");
111 greenArrowModel = LoadGLBModel("green-arrow");
112 }
113
114 void InitLevel(Level *level)
115 {
116 level->seed = (int)(GetTime() * 100.0f);
117
118 TowerInit();
119 EnemyInit();
120 ProjectileInit();
121 ParticleInit();
122 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
123
124 level->placementMode = 0;
125 level->state = LEVEL_STATE_BUILDING;
126 level->nextState = LEVEL_STATE_NONE;
127 level->playerGold = level->initialGold;
128 level->currentWave = 0;
129 level->placementX = -1;
130 level->placementY = 0;
131
132 Camera *camera = &level->camera;
133 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
134 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
135 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
136 camera->fovy = 10.0f;
137 camera->projection = CAMERA_ORTHOGRAPHIC;
138 }
139
140 void DrawLevelHud(Level *level)
141 {
142 const char *text = TextFormat("Gold: %d", level->playerGold);
143 Font font = GetFontDefault();
144 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
145 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
146 }
147
148 void DrawLevelReportLostWave(Level *level)
149 {
150 BeginMode3D(level->camera);
151 DrawLevelGround(level);
152 TowerDraw();
153 EnemyDraw();
154 ProjectileDraw();
155 ParticleDraw();
156 guiState.isBlocked = 0;
157 EndMode3D();
158
159 TowerDrawHealthBars(level->camera);
160
161 const char *text = "Wave lost";
162 int textWidth = MeasureText(text, 20);
163 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
164
165 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
166 {
167 level->nextState = LEVEL_STATE_RESET;
168 }
169 }
170
171 int HasLevelNextWave(Level *level)
172 {
173 for (int i = 0; i < 10; i++)
174 {
175 EnemyWave *wave = &level->waves[i];
176 if (wave->wave == level->currentWave)
177 {
178 return 1;
179 }
180 }
181 return 0;
182 }
183
184 void DrawLevelReportWonWave(Level *level)
185 {
186 BeginMode3D(level->camera);
187 DrawLevelGround(level);
188 TowerDraw();
189 EnemyDraw();
190 ProjectileDraw();
191 ParticleDraw();
192 guiState.isBlocked = 0;
193 EndMode3D();
194
195 TowerDrawHealthBars(level->camera);
196
197 const char *text = "Wave won";
198 int textWidth = MeasureText(text, 20);
199 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
200
201
202 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
203 {
204 level->nextState = LEVEL_STATE_RESET;
205 }
206
207 if (HasLevelNextWave(level))
208 {
209 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
210 {
211 level->nextState = LEVEL_STATE_BUILDING;
212 }
213 }
214 else {
215 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
216 {
217 level->nextState = LEVEL_STATE_WON_LEVEL;
218 }
219 }
220 }
221
222 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
223 {
224 static ButtonState buttonStates[8] = {0};
225 int cost = GetTowerCosts(towerType);
226 const char *text = TextFormat("%s: %d", name, cost);
227 buttonStates[towerType].isSelected = level->placementMode == towerType;
228 buttonStates[towerType].isDisabled = level->playerGold < cost;
229 if (Button(text, x, y, width, height, &buttonStates[towerType]))
230 {
231 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
232 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
233 }
234 }
235
236 float GetRandomFloat(float min, float max)
237 {
238 int random = GetRandomValue(0, 0xfffffff);
239 return ((float)random / (float)0xfffffff) * (max - min) + min;
240 }
241
242 void DrawLevelGround(Level *level)
243 {
244 // draw checkerboard ground pattern
245 for (int x = -5; x <= 5; x += 1)
246 {
247 for (int y = -5; y <= 5; y += 1)
248 {
249 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
250 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
251 }
252 }
253
254 int oldSeed = GetRandomValue(0, 0xfffffff);
255 SetRandomSeed(level->seed);
256 // increase probability for trees via duplicated entries
257 Model borderModels[64];
258 int maxRockCount = GetRandomValue(2, 6);
259 int maxTreeCount = GetRandomValue(10, 20);
260 int maxFirTreeCount = GetRandomValue(5, 10);
261 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
262 int grassPatchCount = GetRandomValue(5, 30);
263
264 int modelCount = 0;
265 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
266 {
267 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
268 }
269 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
270 {
271 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
272 }
273 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
274 {
275 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
276 }
277 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
278 {
279 borderModels[modelCount++] = grassPatchModel[0];
280 }
281
282 // draw some objects around the border of the map
283 Vector3 up = {0, 1, 0};
284 // a pseudo random number generator to get the same result every time
285 const float wiggle = 0.75f;
286 const int layerCount = 3;
287 for (int layer = 0; layer <= layerCount; layer++)
288 {
289 int layerPos = 6 + layer;
290 Model *selectedModels = borderModels;
291 int selectedModelCount = modelCount;
292 if (layer == 0)
293 {
294 selectedModels = grassPatchModel;
295 selectedModelCount = 1;
296 }
297 for (int x = -6 + layer; x <= 6 + layer; x += 1)
298 {
299 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
300 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
301 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
302 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
303 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
304 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
305 }
306
307 for (int z = -5 + layer; z <= 5 + layer; z += 1)
308 {
309 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
310 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
311 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
312 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
313 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
314 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
315 }
316 }
317
318 SetRandomSeed(oldSeed);
319 }
320
321 void DrawEnemyPath(Level *level, Color arrowColor)
322 {
323 const int castleX = 0, castleY = 0;
324 const int maxWaypointCount = 200;
325 const float timeStep = 1.0f;
326 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
327
328 // we start with a time offset to simulate the path,
329 // this way the arrows are animated in a forward moving direction
330 // The time is wrapped around the time step to get a smooth animation
331 float timeOffset = fmodf(GetTime(), timeStep);
332
333 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
334 {
335 EnemyWave *wave = &level->waves[i];
336 if (wave->wave != level->currentWave)
337 {
338 continue;
339 }
340
341 // use this dummy enemy to simulate the path
342 Enemy dummy = {
343 .enemyType = ENEMY_TYPE_MINION,
344 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
345 .nextX = wave->spawnPosition.x,
346 .nextY = wave->spawnPosition.y,
347 .currentX = wave->spawnPosition.x,
348 .currentY = wave->spawnPosition.y,
349 };
350
351 float deltaTime = timeOffset;
352 for (int j = 0; j < maxWaypointCount; j++)
353 {
354 int waypointPassedCount = 0;
355 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
356 // after the initial variable starting offset, we use a fixed time step
357 deltaTime = timeStep;
358 dummy.simPosition = pos;
359
360 // Update the dummy's position just like we do in the regular enemy update loop
361 for (int k = 0; k < waypointPassedCount; k++)
362 {
363 dummy.currentX = dummy.nextX;
364 dummy.currentY = dummy.nextY;
365 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
366 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
367 {
368 break;
369 }
370 }
371 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
372 {
373 break;
374 }
375
376 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
377 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
378 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
379 }
380 }
381 }
382
383 void DrawEnemyPaths(Level *level)
384 {
385 // disable depth testing for the path arrows
386 // flush the 3D batch to draw the arrows on top of everything
387 rlDrawRenderBatchActive();
388 rlDisableDepthTest();
389 DrawEnemyPath(level, (Color){64, 64, 64, 160});
390
391 rlDrawRenderBatchActive();
392 rlEnableDepthTest();
393 DrawEnemyPath(level, WHITE);
394 }
395
396 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
397 {
398 float dt = gameTime.fixedDeltaTime;
399 // smooth transition for the placement position using exponential decay
400 const float lambda = 15.0f;
401 float factor = 1.0f - expf(-lambda * dt);
402
403 float damping = 0.5f;
404 float springStiffness = 300.0f;
405 float springDecay = 95.0f;
406 float minHeight = 0.35f;
407
408 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
409 {
410 damping = 1.0f;
411 springDecay = 90.0f;
412 springStiffness = 100.0f;
413 minHeight = 0.70f;
414 }
415
416 for (int i = 0; i < gameTime.fixedStepCount; i++)
417 {
418 level->placementTransitionPosition =
419 Vector2Lerp(
420 level->placementTransitionPosition,
421 (Vector2){mapX, mapY}, factor);
422
423 // draw the spring position for debugging the spring simulation
424 // first step: stiff spring, no simulation
425 Vector3 worldPlacementPosition = (Vector3){
426 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
427 Vector3 springTargetPosition = (Vector3){
428 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
429 // consider the current velocity to predict the future position in order to dampen
430 // the spring simulation. Longer prediction times will result in more damping
431 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
432 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
433 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
434 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
435 // decay velocity of the upright forcing spring
436 // This force acts like a 2nd spring that pulls the tip upright into the air above the
437 // base position
438 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
439 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
440
441 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
442 // we use a simple spring model with a rest length of 1.0f
443 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
444 float springLength = Vector3Length(springDelta);
445 float springForce = (springLength - 1.0f) * springStiffness;
446 Vector3 springForceVector = Vector3Normalize(springDelta);
447 springForceVector = Vector3Scale(springForceVector, springForce);
448 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
449 Vector3Scale(springForceVector, dt));
450
451 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
452 Vector3Scale(level->placementTowerSpring.velocity, dt));
453 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
454 {
455 level->placementTowerSpring.velocity.y *= -1.0f;
456 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
457 }
458 }
459 }
460
461 void DrawLevelBuildingPlacementState(Level *level)
462 {
463 const float placementDuration = 0.5f;
464
465 level->placementTimer += gameTime.deltaTime;
466 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
467 {
468 level->placementPhase = PLACEMENT_PHASE_MOVING;
469 level->placementTimer = 0.0f;
470 }
471
472 BeginMode3D(level->camera);
473 DrawLevelGround(level);
474
475 int blockedCellCount = 0;
476 Vector2 blockedCells[1];
477 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
478 float planeDistance = ray.position.y / -ray.direction.y;
479 float planeX = ray.direction.x * planeDistance + ray.position.x;
480 float planeY = ray.direction.z * planeDistance + ray.position.z;
481 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
482 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
483 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
484 level->placementMode && !guiState.isBlocked &&
485 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
486 {
487 level->placementX = mapX;
488 level->placementY = mapY;
489 }
490 else
491 {
492 mapX = level->placementX;
493 mapY = level->placementY;
494 }
495 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
496 PathFindingMapUpdate(blockedCellCount, blockedCells);
497
498 TowerDraw();
499 EnemyDraw();
500 ProjectileDraw();
501 ParticleDraw();
502 DrawEnemyPaths(level);
503
504 // let the tower float up and down. Consider this height in the spring simulation as well
505 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
506
507 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
508 {
509 // The bouncing spring needs a bit of outro time to look nice and complete.
510 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
511 // and the last 1/3rd is the outro physics (bouncing)
512 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
513 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
514 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
515 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
516 towerFloatHeight = linearBlendHeight + parabola;
517 }
518
519 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
520
521 rlPushMatrix();
522 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
523
524 rlPushMatrix();
525 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
526 // calculate x and z rotation to align the model with the spring
527 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
528 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
529 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
530 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
531 float springLength = Vector3Length(towerUp);
532 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
533 float towerSquash = 1.0f / towerStretch;
534 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
535 rlScalef(towerSquash, towerStretch, towerSquash);
536 Tower dummy = {
537 .towerType = level->placementMode,
538 };
539 TowerDrawSingle(dummy);
540 rlPopMatrix();
541
542 // draw a shadow for the tower
543 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
544 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
545 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
546
547
548 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
549 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
550 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
551 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
552
553 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
554 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
555 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
556 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
557 rlPopMatrix();
558
559 guiState.isBlocked = 0;
560
561 EndMode3D();
562
563 TowerDrawHealthBars(level->camera);
564
565 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
566 {
567 if (level->placementTimer > placementDuration)
568 {
569 TowerTryAdd(level->placementMode, mapX, mapY);
570 level->playerGold -= GetTowerCosts(level->placementMode);
571 level->nextState = LEVEL_STATE_BUILDING;
572 level->placementMode = TOWER_TYPE_NONE;
573 }
574 }
575 else
576 {
577 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
578 {
579 level->nextState = LEVEL_STATE_BUILDING;
580 level->placementMode = TOWER_TYPE_NONE;
581 TraceLog(LOG_INFO, "Cancel building");
582 }
583
584 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
585 {
586 level->placementPhase = PLACEMENT_PHASE_PLACING;
587 level->placementTimer = 0.0f;
588 }
589 }
590 }
591
592 void DrawLevelBuildingState(Level *level)
593 {
594 BeginMode3D(level->camera);
595 DrawLevelGround(level);
596
597 PathFindingMapUpdate(0, 0);
598 TowerDraw();
599 EnemyDraw();
600 ProjectileDraw();
601 ParticleDraw();
602 DrawEnemyPaths(level);
603
604 guiState.isBlocked = 0;
605
606 EndMode3D();
607
608 TowerDrawHealthBars(level->camera);
609
610 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
611 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
612 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
613 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
614
615 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
616 {
617 level->nextState = LEVEL_STATE_RESET;
618 }
619
620 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
621 {
622 level->nextState = LEVEL_STATE_BATTLE;
623 }
624
625 const char *text = "Building phase";
626 int textWidth = MeasureText(text, 20);
627 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
628 }
629
630 void InitBattleStateConditions(Level *level)
631 {
632 level->state = LEVEL_STATE_BATTLE;
633 level->nextState = LEVEL_STATE_NONE;
634 level->waveEndTimer = 0.0f;
635 for (int i = 0; i < 10; i++)
636 {
637 EnemyWave *wave = &level->waves[i];
638 wave->spawned = 0;
639 wave->timeToSpawnNext = wave->delay;
640 }
641 }
642
643 void DrawLevelBattleState(Level *level)
644 {
645 BeginMode3D(level->camera);
646 DrawLevelGround(level);
647 TowerDraw();
648 EnemyDraw();
649 ProjectileDraw();
650 ParticleDraw();
651 guiState.isBlocked = 0;
652 EndMode3D();
653
654 EnemyDrawHealthbars(level->camera);
655 TowerDrawHealthBars(level->camera);
656
657 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
658 {
659 level->nextState = LEVEL_STATE_RESET;
660 }
661
662 int maxCount = 0;
663 int remainingCount = 0;
664 for (int i = 0; i < 10; i++)
665 {
666 EnemyWave *wave = &level->waves[i];
667 if (wave->wave != level->currentWave)
668 {
669 continue;
670 }
671 maxCount += wave->count;
672 remainingCount += wave->count - wave->spawned;
673 }
674 int aliveCount = EnemyCount();
675 remainingCount += aliveCount;
676
677 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
678 int textWidth = MeasureText(text, 20);
679 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
680 }
681
682 void DrawLevel(Level *level)
683 {
684 switch (level->state)
685 {
686 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
687 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
688 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
689 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
690 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
691 default: break;
692 }
693
694 DrawLevelHud(level);
695 }
696
697 void UpdateLevel(Level *level)
698 {
699 if (level->state == LEVEL_STATE_BATTLE)
700 {
701 int activeWaves = 0;
702 for (int i = 0; i < 10; i++)
703 {
704 EnemyWave *wave = &level->waves[i];
705 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
706 {
707 continue;
708 }
709 activeWaves++;
710 wave->timeToSpawnNext -= gameTime.deltaTime;
711 if (wave->timeToSpawnNext <= 0.0f)
712 {
713 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
714 if (enemy)
715 {
716 wave->timeToSpawnNext = wave->interval;
717 wave->spawned++;
718 }
719 }
720 }
721 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
722 level->waveEndTimer += gameTime.deltaTime;
723 if (level->waveEndTimer >= 2.0f)
724 {
725 level->nextState = LEVEL_STATE_LOST_WAVE;
726 }
727 }
728 else if (activeWaves == 0 && EnemyCount() == 0)
729 {
730 level->waveEndTimer += gameTime.deltaTime;
731 if (level->waveEndTimer >= 2.0f)
732 {
733 level->nextState = LEVEL_STATE_WON_WAVE;
734 }
735 }
736 }
737
738 PathFindingMapUpdate(0, 0);
739 EnemyUpdate();
740 TowerUpdate();
741 ProjectileUpdate();
742 ParticleUpdate();
743
744 if (level->nextState == LEVEL_STATE_RESET)
745 {
746 InitLevel(level);
747 }
748
749 if (level->nextState == LEVEL_STATE_BATTLE)
750 {
751 InitBattleStateConditions(level);
752 }
753
754 if (level->nextState == LEVEL_STATE_WON_WAVE)
755 {
756 level->currentWave++;
757 level->state = LEVEL_STATE_WON_WAVE;
758 }
759
760 if (level->nextState == LEVEL_STATE_LOST_WAVE)
761 {
762 level->state = LEVEL_STATE_LOST_WAVE;
763 }
764
765 if (level->nextState == LEVEL_STATE_BUILDING)
766 {
767 level->state = LEVEL_STATE_BUILDING;
768 }
769
770 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
771 {
772 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
773 level->placementTransitionPosition = (Vector2){
774 level->placementX, level->placementY};
775 // initialize the spring to the current position
776 level->placementTowerSpring = (PhysicsPoint){
777 .position = (Vector3){level->placementX, 8.0f, level->placementY},
778 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
779 };
780 level->placementPhase = PLACEMENT_PHASE_STARTING;
781 level->placementTimer = 0.0f;
782 }
783
784 if (level->nextState == LEVEL_STATE_WON_LEVEL)
785 {
786 // make something of this later
787 InitLevel(level);
788 }
789
790 level->nextState = LEVEL_STATE_NONE;
791 }
792
793 float nextSpawnTime = 0.0f;
794
795 void ResetGame()
796 {
797 InitLevel(currentLevel);
798 }
799
800 void InitGame()
801 {
802 TowerInit();
803 EnemyInit();
804 ProjectileInit();
805 ParticleInit();
806 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
807
808 currentLevel = levels;
809 InitLevel(currentLevel);
810 }
811
812 //# Immediate GUI functions
813
814 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
815 {
816 const float healthBarHeight = 6.0f;
817 const float healthBarOffset = 15.0f;
818 const float inset = 2.0f;
819 const float innerWidth = healthBarWidth - inset * 2;
820 const float innerHeight = healthBarHeight - inset * 2;
821
822 Vector2 screenPos = GetWorldToScreen(position, camera);
823 screenPos = Vector2Add(screenPos, screenOffset);
824 float centerX = screenPos.x - healthBarWidth * 0.5f;
825 float topY = screenPos.y - healthBarOffset;
826 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
827 float healthWidth = innerWidth * healthRatio;
828 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
829 }
830
831 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
832 {
833 Rectangle bounds = {x, y, width, height};
834 int isPressed = 0;
835 int isSelected = state && state->isSelected;
836 int isDisabled = state && state->isDisabled;
837 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
838 {
839 Color color = isSelected ? DARKGRAY : GRAY;
840 DrawRectangle(x, y, width, height, color);
841 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
842 {
843 isPressed = 1;
844 }
845 guiState.isBlocked = 1;
846 }
847 else
848 {
849 Color color = isSelected ? WHITE : LIGHTGRAY;
850 DrawRectangle(x, y, width, height, color);
851 }
852 Font font = GetFontDefault();
853 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
854 Color textColor = isDisabled ? GRAY : BLACK;
855 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
856 return isPressed;
857 }
858
859 //# Main game loop
860
861 void GameUpdate()
862 {
863 UpdateLevel(currentLevel);
864 }
865
866 int main(void)
867 {
868 int screenWidth, screenHeight;
869 GetPreferredSize(&screenWidth, &screenHeight);
870 InitWindow(screenWidth, screenHeight, "Tower defense");
871 float gamespeed = 1.0f;
872 SetTargetFPS(30);
873
874 LoadAssets();
875 InitGame();
876
877 float pause = 1.0f;
878
879 while (!WindowShouldClose())
880 {
881 if (IsPaused()) {
882 // canvas is not visible in browser - do nothing
883 continue;
884 }
885
886 if (IsKeyPressed(KEY_T))
887 {
888 gamespeed += 0.1f;
889 if (gamespeed > 1.05f) gamespeed = 0.1f;
890 }
891
892 if (IsKeyPressed(KEY_P))
893 {
894 pause = pause > 0.5f ? 0.0f : 1.0f;
895 }
896
897 float dt = GetFrameTime() * gamespeed * pause;
898 // cap maximum delta time to 0.1 seconds to prevent large time steps
899 if (dt > 0.1f) dt = 0.1f;
900 gameTime.time += dt;
901 gameTime.deltaTime = dt;
902 gameTime.frameCount += 1;
903
904 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
905 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
906
907 BeginDrawing();
908 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
909
910 GameUpdate();
911 DrawLevel(currentLevel);
912
913 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
914 EndDrawing();
915
916 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
917 }
918
919 CloseWindow();
920
921 return 0;
922 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 8.0f,
32 .speed = 0.5f,
33 .radius = 0.25f,
34 .maxAcceleration = 1.0f,
35 .explosionDamage = 2.0f,
36 .requiredContactTime = 0.5f,
37 .explosionRange = 1.0f,
38 .explosionPushbackPower = 0.25f,
39 .goldValue = 3,
40 .shieldDamageAbsorption = 4.0f,
41 .shieldHealth = 25.0f,
42 },
43 [ENEMY_TYPE_BOSS] = {
44 .health = 50.0f,
45 .speed = 0.4f,
46 .radius = 0.25f,
47 .maxAcceleration = 1.0f,
48 .explosionDamage = 5.0f,
49 .requiredContactTime = 0.5f,
50 .explosionRange = 1.0f,
51 .explosionPushbackPower = 0.25f,
52 .goldValue = 10,
53 },
54 };
55
56 Enemy enemies[ENEMY_MAX_COUNT];
57 int enemyCount = 0;
58
59 SpriteUnit enemySprites[] = {
60 [ENEMY_TYPE_MINION] = {
61 .animations[0] = {
62 .srcRect = {0, 17, 16, 15},
63 .offset = {8.0f, 0.0f},
64 .frameCount = 6,
65 .frameDuration = 0.1f,
66 },
67 .animations[1] = {
68 .srcRect = {1, 33, 15, 14},
69 .offset = {7.0f, 0.0f},
70 .frameCount = 6,
71 .frameWidth = 16,
72 .frameDuration = 0.1f,
73 },
74 },
75 [ENEMY_TYPE_RUNNER] = {
76 .scale = 0.75f,
77 .animations[0] = {
78 .srcRect = {0, 17, 16, 15},
79 .offset = {8.0f, 0.0f},
80 .frameCount = 6,
81 .frameDuration = 0.1f,
82 },
83 },
84 [ENEMY_TYPE_SHIELD] = {
85 .animations[0] = {
86 .srcRect = {0, 17, 16, 15},
87 .offset = {8.0f, 0.0f},
88 .frameCount = 6,
89 .frameDuration = 0.1f,
90 },
91 .animations[1] = {
92 .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
93 .srcRect = {99, 17, 10, 11},
94 .offset = {7.0f, 0.0f},
95 },
96 },
97 [ENEMY_TYPE_BOSS] = {
98 .scale = 1.5f,
99 .animations[0] = {
100 .srcRect = {0, 17, 16, 15},
101 .offset = {8.0f, 0.0f},
102 .frameCount = 6,
103 .frameDuration = 0.1f,
104 },
105 .animations[1] = {
106 .srcRect = {97, 29, 14, 7},
107 .offset = {7.0f, -9.0f},
108 },
109 },
110 };
111
112 void EnemyInit()
113 {
114 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115 {
116 enemies[i] = (Enemy){0};
117 }
118 enemyCount = 0;
119 }
120
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123 return enemyClassConfigs[enemy->enemyType].speed;
124 }
125
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128 return enemyClassConfigs[enemy->enemyType].health;
129 }
130
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133 int16_t castleX = 0;
134 int16_t castleY = 0;
135 int16_t dx = castleX - currentX;
136 int16_t dy = castleY - currentY;
137 if (dx == 0 && dy == 0)
138 {
139 *nextX = currentX;
140 *nextY = currentY;
141 return 1;
142 }
143 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144
145 if (gradient.x == 0 && gradient.y == 0)
146 {
147 *nextX = currentX;
148 *nextY = currentY;
149 return 1;
150 }
151
152 if (fabsf(gradient.x) > fabsf(gradient.y))
153 {
154 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155 *nextY = currentY;
156 return 0;
157 }
158 *nextX = currentX;
159 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160 return 0;
161 }
162
163
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167 const float pointReachedDistance = 0.25f;
168 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169 const float maxSimStepTime = 0.015625f;
170
171 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173 int16_t nextX = enemy->nextX;
174 int16_t nextY = enemy->nextY;
175 Vector2 position = enemy->simPosition;
176 int passedCount = 0;
177 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178 {
179 float stepTime = fminf(deltaT - t, maxSimStepTime);
180 Vector2 target = (Vector2){nextX, nextY};
181 float speed = Vector2Length(*velocity);
182 // draw the target position for debugging
183 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186 {
187 // we reached the target position, let's move to the next waypoint
188 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189 target = (Vector2){nextX, nextY};
190 // track how many waypoints we passed
191 passedCount++;
192 }
193
194 // acceleration towards the target
195 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197 *velocity = Vector2Add(*velocity, acceleration);
198
199 // limit the speed to the maximum speed
200 if (speed > maxSpeed)
201 {
202 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203 }
204
205 // move the enemy
206 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207 }
208
209 if (waypointPassedCount)
210 {
211 (*waypointPassedCount) = passedCount;
212 }
213
214 return position;
215 }
216
217 void EnemyDraw()
218 {
219 rlDrawRenderBatchActive();
220 rlDisableDepthMask();
221 for (int i = 0; i < enemyCount; i++)
222 {
223 Enemy enemy = enemies[i];
224 if (enemy.enemyType == ENEMY_TYPE_NONE)
225 {
226 continue;
227 }
228
229 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230
231 // don't draw any trails for now; might replace this with footprints later
232 // if (enemy.movePathCount > 0)
233 // {
234 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236 // }
237 // for (int j = 1; j < enemy.movePathCount; j++)
238 // {
239 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241 // DrawLine3D(p, q, GREEN);
242 // }
243
244 float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245 int phase = 0;
246 if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247 {
248 phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249 }
250
251 switch (enemy.enemyType)
252 {
253 case ENEMY_TYPE_MINION:
254 case ENEMY_TYPE_RUNNER:
255 case ENEMY_TYPE_SHIELD:
256 case ENEMY_TYPE_BOSS:
257 DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y},
258 enemy.walkedDistance, 0, phase);
259 break;
260 }
261 }
262 rlDrawRenderBatchActive();
263 rlEnableDepthMask();
264 }
265
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268 // damage the tower
269 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272 float explosionRange2 = explosionRange * explosionRange;
273 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274 // explode the enemy
275 if (tower->damage >= TowerGetMaxHealth(tower))
276 {
277 tower->towerType = TOWER_TYPE_NONE;
278 }
279
280 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
281 explosionSource,
282 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283
284 enemy->enemyType = ENEMY_TYPE_NONE;
285
286 // push back enemies & dealing damage
287 for (int i = 0; i < enemyCount; i++)
288 {
289 Enemy *other = &enemies[i];
290 if (other->enemyType == ENEMY_TYPE_NONE)
291 {
292 continue;
293 }
294 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295 if (distanceSqr > 0 && distanceSqr < explosionRange2)
296 {
297 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299 EnemyAddDamage(other, explosionDamge);
300 }
301 }
302 }
303
304 void EnemyUpdate()
305 {
306 const float castleX = 0;
307 const float castleY = 0;
308 const float maxPathDistance2 = 0.25f * 0.25f;
309
310 for (int i = 0; i < enemyCount; i++)
311 {
312 Enemy *enemy = &enemies[i];
313 if (enemy->enemyType == ENEMY_TYPE_NONE)
314 {
315 continue;
316 }
317
318 int waypointPassedCount = 0;
319 Vector2 prevPosition = enemy->simPosition;
320 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321 enemy->startMovingTime = gameTime.time;
322 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323 // track path of unit
324 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325 {
326 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327 {
328 enemy->movePath[j] = enemy->movePath[j - 1];
329 }
330 enemy->movePath[0] = enemy->simPosition;
331 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332 {
333 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334 }
335 }
336
337 if (waypointPassedCount > 0)
338 {
339 enemy->currentX = enemy->nextX;
340 enemy->currentY = enemy->nextY;
341 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343 {
344 // enemy reached the castle; remove it
345 enemy->enemyType = ENEMY_TYPE_NONE;
346 continue;
347 }
348 }
349 }
350
351 // handle collisions between enemies
352 for (int i = 0; i < enemyCount - 1; i++)
353 {
354 Enemy *enemyA = &enemies[i];
355 if (enemyA->enemyType == ENEMY_TYPE_NONE)
356 {
357 continue;
358 }
359 for (int j = i + 1; j < enemyCount; j++)
360 {
361 Enemy *enemyB = &enemies[j];
362 if (enemyB->enemyType == ENEMY_TYPE_NONE)
363 {
364 continue;
365 }
366 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369 float radiusSum = radiusA + radiusB;
370 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371 {
372 // collision
373 float distance = sqrtf(distanceSqr);
374 float overlap = radiusSum - distance;
375 // move the enemies apart, but softly; if we have a clog of enemies,
376 // moving them perfectly apart can cause them to jitter
377 float positionCorrection = overlap / 5.0f;
378 Vector2 direction = (Vector2){
379 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383 }
384 }
385 }
386
387 // handle collisions between enemies and towers
388 for (int i = 0; i < enemyCount; i++)
389 {
390 Enemy *enemy = &enemies[i];
391 if (enemy->enemyType == ENEMY_TYPE_NONE)
392 {
393 continue;
394 }
395 enemy->contactTime -= gameTime.deltaTime;
396 if (enemy->contactTime < 0.0f)
397 {
398 enemy->contactTime = 0.0f;
399 }
400
401 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402 // linear search over towers; could be optimized by using path finding tower map,
403 // but for now, we keep it simple
404 for (int j = 0; j < towerCount; j++)
405 {
406 Tower *tower = &towers[j];
407 if (tower->towerType == TOWER_TYPE_NONE)
408 {
409 continue;
410 }
411 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413 if (distanceSqr > combinedRadius * combinedRadius)
414 {
415 continue;
416 }
417 // potential collision; square / circle intersection
418 float dx = tower->x - enemy->simPosition.x;
419 float dy = tower->y - enemy->simPosition.y;
420 float absDx = fabsf(dx);
421 float absDy = fabsf(dy);
422 Vector3 contactPoint = {0};
423 if (absDx <= 0.5f && absDx <= absDy) {
424 // vertical collision; push the enemy out horizontally
425 float overlap = enemyRadius + 0.5f - absDy;
426 if (overlap < 0.0f)
427 {
428 continue;
429 }
430 float direction = dy > 0.0f ? -1.0f : 1.0f;
431 enemy->simPosition.y += direction * overlap;
432 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433 }
434 else if (absDy <= 0.5f && absDy <= absDx)
435 {
436 // horizontal collision; push the enemy out vertically
437 float overlap = enemyRadius + 0.5f - absDx;
438 if (overlap < 0.0f)
439 {
440 continue;
441 }
442 float direction = dx > 0.0f ? -1.0f : 1.0f;
443 enemy->simPosition.x += direction * overlap;
444 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445 }
446 else
447 {
448 // possible collision with a corner
449 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451 float cornerX = tower->x + cornerDX;
452 float cornerY = tower->y + cornerDY;
453 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454 if (cornerDistanceSqr > enemyRadius * enemyRadius)
455 {
456 continue;
457 }
458 // push the enemy out along the diagonal
459 float cornerDistance = sqrtf(cornerDistanceSqr);
460 float overlap = enemyRadius - cornerDistance;
461 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463 enemy->simPosition.x -= directionX * overlap;
464 enemy->simPosition.y -= directionY * overlap;
465 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466 }
467
468 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469 {
470 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472 {
473 EnemyTriggerExplode(enemy, tower, contactPoint);
474 }
475 }
476 }
477 }
478 }
479
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482 return (EnemyId){enemy - enemies, enemy->generation};
483 }
484
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487 if (enemyId.index >= ENEMY_MAX_COUNT)
488 {
489 return 0;
490 }
491 Enemy *enemy = &enemies[enemyId.index];
492 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493 {
494 return 0;
495 }
496 return enemy;
497 }
498
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501 Enemy *spawn = 0;
502 for (int i = 0; i < enemyCount; i++)
503 {
504 Enemy *enemy = &enemies[i];
505 if (enemy->enemyType == ENEMY_TYPE_NONE)
506 {
507 spawn = enemy;
508 break;
509 }
510 }
511
512 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513 {
514 spawn = &enemies[enemyCount++];
515 }
516
517 if (spawn)
518 {
519 *spawn = (Enemy){0};
520 spawn->currentX = currentX;
521 spawn->currentY = currentY;
522 spawn->nextX = currentX;
523 spawn->nextY = currentY;
524 spawn->simPosition = (Vector2){currentX, currentY};
525 spawn->simVelocity = (Vector2){0, 0};
526 spawn->enemyType = enemyType;
527 spawn->startMovingTime = gameTime.time;
528 spawn->damage = 0.0f;
529 spawn->futureDamage = 0.0f;
530 spawn->generation++;
531 spawn->movePathCount = 0;
532 spawn->walkedDistance = 0.0f;
533 }
534
535 return spawn;
536 }
537
538 int EnemyAddDamageRange(Vector2 position, float range, float damage)
539 {
540 int count = 0;
541 float range2 = range * range;
542 for (int i = 0; i < enemyCount; i++)
543 {
544 Enemy *enemy = &enemies[i];
545 if (enemy->enemyType == ENEMY_TYPE_NONE)
546 {
547 continue;
548 }
549 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
550 if (distance2 <= range2)
551 {
552 EnemyAddDamage(enemy, damage);
553 count++;
554 }
555 }
556 return count;
557 }
558
559 int EnemyAddDamage(Enemy *enemy, float damage)
560 {
561 float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
562 if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
563 {
564 float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
565 float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
566 enemy->shieldDamage += shieldDamage;
567 damage -= shieldDamage;
568 }
569 enemy->damage += damage;
570 if (enemy->damage >= EnemyGetMaxHealth(enemy))
571 {
572 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
573 enemy->enemyType = ENEMY_TYPE_NONE;
574 return 1;
575 }
576
577 return 0;
578 }
579
580 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
581 {
582 int16_t castleX = 0;
583 int16_t castleY = 0;
584 Enemy* closest = 0;
585 int16_t closestDistance = 0;
586 float range2 = range * range;
587 for (int i = 0; i < enemyCount; i++)
588 {
589 Enemy* enemy = &enemies[i];
590 if (enemy->enemyType == ENEMY_TYPE_NONE)
591 {
592 continue;
593 }
594 float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
595 if (enemy->futureDamage >= maxHealth)
596 {
597 // ignore enemies that will die soon
598 continue;
599 }
600 int16_t dx = castleX - enemy->currentX;
601 int16_t dy = castleY - enemy->currentY;
602 int16_t distance = abs(dx) + abs(dy);
603 if (!closest || distance < closestDistance)
604 {
605 float tdx = towerX - enemy->currentX;
606 float tdy = towerY - enemy->currentY;
607 float tdistance2 = tdx * tdx + tdy * tdy;
608 if (tdistance2 <= range2)
609 {
610 closest = enemy;
611 closestDistance = distance;
612 }
613 }
614 }
615 return closest;
616 }
617
618 int EnemyCount()
619 {
620 int count = 0;
621 for (int i = 0; i < enemyCount; i++)
622 {
623 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
624 {
625 count++;
626 }
627 }
628 return count;
629 }
630
631 void EnemyDrawHealthbars(Camera3D camera)
632 {
633 for (int i = 0; i < enemyCount; i++)
634 {
635 Enemy *enemy = &enemies[i];
636
637 float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
638 if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
639 {
640 float shieldHealth = maxShieldHealth - enemy->shieldDamage;
641 float shieldHealthRatio = shieldHealth / maxShieldHealth;
642 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
643 DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
644 }
645
646 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
647 {
648 continue;
649 }
650 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
651 float maxHealth = EnemyGetMaxHealth(enemy);
652 float health = maxHealth - enemy->damage;
653 float healthRatio = health / maxHealth;
654
655 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
656 }
657 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 typedef struct PhysicsPoint
12 {
13 Vector3 position;
14 Vector3 velocity;
15 } PhysicsPoint;
16
17 #define ENEMY_MAX_PATH_COUNT 8
18 #define ENEMY_MAX_COUNT 400
19 #define ENEMY_TYPE_NONE 0
20
21 #define ENEMY_TYPE_MINION 1
22 #define ENEMY_TYPE_RUNNER 2
23 #define ENEMY_TYPE_SHIELD 3
24 #define ENEMY_TYPE_BOSS 4
25
26 #define PARTICLE_MAX_COUNT 400
27 #define PARTICLE_TYPE_NONE 0
28 #define PARTICLE_TYPE_EXPLOSION 1
29
30 typedef struct Particle
31 {
32 uint8_t particleType;
33 float spawnTime;
34 float lifetime;
35 Vector3 position;
36 Vector3 velocity;
37 Vector3 scale;
38 } Particle;
39
40 #define TOWER_MAX_COUNT 400
41 enum TowerType
42 {
43 TOWER_TYPE_NONE,
44 TOWER_TYPE_BASE,
45 TOWER_TYPE_ARCHER,
46 TOWER_TYPE_BALLISTA,
47 TOWER_TYPE_CATAPULT,
48 TOWER_TYPE_WALL,
49 TOWER_TYPE_COUNT
50 };
51
52 typedef struct HitEffectConfig
53 {
54 float damage;
55 float areaDamageRadius;
56 float pushbackPowerDistance;
57 } HitEffectConfig;
58
59 typedef struct TowerTypeConfig
60 {
61 float cooldown;
62 float range;
63 float projectileSpeed;
64
65 uint8_t cost;
66 uint8_t projectileType;
67 uint16_t maxHealth;
68
69 HitEffectConfig hitEffect;
70 } TowerTypeConfig;
71
72 typedef struct Tower
73 {
74 int16_t x, y;
75 uint8_t towerType;
76 Vector2 lastTargetPosition;
77 float cooldown;
78 float damage;
79 } Tower;
80
81 typedef struct GameTime
82 {
83 float time;
84 float deltaTime;
85 uint32_t frameCount;
86
87 float fixedDeltaTime;
88 // leaving the fixed time stepping to the update functions,
89 // we need to know the fixed time at the start of the frame
90 float fixedTimeStart;
91 // and the number of fixed steps that we have to make this frame
92 // The fixedTime is fixedTimeStart + n * fixedStepCount
93 uint8_t fixedStepCount;
94 } GameTime;
95
96 typedef struct ButtonState {
97 char isSelected;
98 char isDisabled;
99 } ButtonState;
100
101 typedef struct GUIState {
102 int isBlocked;
103 } GUIState;
104
105 typedef enum LevelState
106 {
107 LEVEL_STATE_NONE,
108 LEVEL_STATE_BUILDING,
109 LEVEL_STATE_BUILDING_PLACEMENT,
110 LEVEL_STATE_BATTLE,
111 LEVEL_STATE_WON_WAVE,
112 LEVEL_STATE_LOST_WAVE,
113 LEVEL_STATE_WON_LEVEL,
114 LEVEL_STATE_RESET,
115 } LevelState;
116
117 typedef struct EnemyWave {
118 uint8_t enemyType;
119 uint8_t wave;
120 uint16_t count;
121 float interval;
122 float delay;
123 Vector2 spawnPosition;
124
125 uint16_t spawned;
126 float timeToSpawnNext;
127 } EnemyWave;
128
129 #define ENEMY_MAX_WAVE_COUNT 10
130
131 typedef enum PlacementPhase
132 {
133 PLACEMENT_PHASE_STARTING,
134 PLACEMENT_PHASE_MOVING,
135 PLACEMENT_PHASE_PLACING,
136 } PlacementPhase;
137
138 typedef struct Level
139 {
140 int seed;
141 LevelState state;
142 LevelState nextState;
143 Camera3D camera;
144 int placementMode;
145 PlacementPhase placementPhase;
146 float placementTimer;
147 int16_t placementX;
148 int16_t placementY;
149 Vector2 placementTransitionPosition;
150 PhysicsPoint placementTowerSpring;
151
152 int initialGold;
153 int playerGold;
154
155 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
156 int currentWave;
157 float waveEndTimer;
158 } Level;
159
160 typedef struct DeltaSrc
161 {
162 char x, y;
163 } DeltaSrc;
164
165 typedef struct PathfindingMap
166 {
167 int width, height;
168 float scale;
169 float *distances;
170 long *towerIndex;
171 DeltaSrc *deltaSrc;
172 float maxDistance;
173 Matrix toMapSpace;
174 Matrix toWorldSpace;
175 } PathfindingMap;
176
177 // when we execute the pathfinding algorithm, we need to store the active nodes
178 // in a queue. Each node has a position, a distance from the start, and the
179 // position of the node that we came from.
180 typedef struct PathfindingNode
181 {
182 int16_t x, y, fromX, fromY;
183 float distance;
184 } PathfindingNode;
185
186 typedef struct EnemyId
187 {
188 uint16_t index;
189 uint16_t generation;
190 } EnemyId;
191
192 typedef struct EnemyClassConfig
193 {
194 float speed;
195 float health;
196 float shieldHealth;
197 float shieldDamageAbsorption;
198 float radius;
199 float maxAcceleration;
200 float requiredContactTime;
201 float explosionDamage;
202 float explosionRange;
203 float explosionPushbackPower;
204 int goldValue;
205 } EnemyClassConfig;
206
207 typedef struct Enemy
208 {
209 int16_t currentX, currentY;
210 int16_t nextX, nextY;
211 Vector2 simPosition;
212 Vector2 simVelocity;
213 uint16_t generation;
214 float walkedDistance;
215 float startMovingTime;
216 float damage, futureDamage;
217 float shieldDamage;
218 float contactTime;
219 uint8_t enemyType;
220 uint8_t movePathCount;
221 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
222 } Enemy;
223
224 // a unit that uses sprites to be drawn
225 #define SPRITE_UNIT_ANIMATION_COUNT 6
226 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
227 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
228 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
229
230 typedef struct SpriteAnimation
231 {
232 Rectangle srcRect;
233 Vector2 offset;
234 uint8_t animationId;
235 uint8_t frameCount;
236 uint8_t frameWidth;
237 float frameDuration;
238 } SpriteAnimation;
239
240 typedef struct SpriteUnit
241 {
242 float scale;
243 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
244 } SpriteUnit;
245
246 #define PROJECTILE_MAX_COUNT 1200
247 #define PROJECTILE_TYPE_NONE 0
248 #define PROJECTILE_TYPE_ARROW 1
249 #define PROJECTILE_TYPE_CATAPULT 2
250 #define PROJECTILE_TYPE_BALLISTA 3
251
252 typedef struct Projectile
253 {
254 uint8_t projectileType;
255 float shootTime;
256 float arrivalTime;
257 float distance;
258 Vector3 position;
259 Vector3 target;
260 Vector3 directionNormal;
261 EnemyId targetEnemy;
262 HitEffectConfig hitEffectConfig;
263 } Projectile;
264
265 //# Function declarations
266 float TowerGetMaxHealth(Tower *tower);
267 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
268 int EnemyAddDamageRange(Vector2 position, float range, float damage);
269 int EnemyAddDamage(Enemy *enemy, float damage);
270
271 //# Enemy functions
272 void EnemyInit();
273 void EnemyDraw();
274 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
275 void EnemyUpdate();
276 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
277 float EnemyGetMaxHealth(Enemy *enemy);
278 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
279 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
280 EnemyId EnemyGetId(Enemy *enemy);
281 Enemy *EnemyTryResolve(EnemyId enemyId);
282 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
283 int EnemyAddDamage(Enemy *enemy, float damage);
284 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
285 int EnemyCount();
286 void EnemyDrawHealthbars(Camera3D camera);
287
288 //# Tower functions
289 void TowerInit();
290 Tower *TowerGetAt(int16_t x, int16_t y);
291 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
292 Tower *GetTowerByType(uint8_t towerType);
293 int GetTowerCosts(uint8_t towerType);
294 float TowerGetMaxHealth(Tower *tower);
295 void TowerDraw();
296 void TowerDrawSingle(Tower tower);
297 void TowerUpdate();
298 void TowerDrawHealthBars(Camera3D camera);
299 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
300
301 //# Particles
302 void ParticleInit();
303 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
304 void ParticleUpdate();
305 void ParticleDraw();
306
307 //# Projectiles
308 void ProjectileInit();
309 void ProjectileDraw();
310 void ProjectileUpdate();
311 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
312
313 //# Pathfinding map
314 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
315 float PathFindingGetDistance(int mapX, int mapY);
316 Vector2 PathFindingGetGradient(Vector3 world);
317 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
318 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
319 void PathFindingMapDraw();
320
321 //# UI
322 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
323
324 //# Level
325 void DrawLevelGround(Level *level);
326 void DrawEnemyPath(Level *level, Color arrowColor);
327
328 //# variables
329 extern Level *currentLevel;
330 extern Enemy enemies[ENEMY_MAX_COUNT];
331 extern int enemyCount;
332 extern EnemyClassConfig enemyClassConfigs[];
333
334 extern GUIState guiState;
335 extern GameTime gameTime;
336 extern Tower towers[TOWER_MAX_COUNT];
337 extern int towerCount;
338
339 extern Texture2D palette, spriteSheet;
340
341 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 8.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .animations[0] = {
57 .srcRect = {0, 0, 16, 16},
58 .offset = {7, 1},
59 .frameCount = 1,
60 .frameDuration = 0.0f,
61 },
62 .animations[1] = {
63 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
64 .srcRect = {16, 0, 6, 16},
65 .offset = {8, 0},
66 },
67 .animations[2] = {
68 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
69 .srcRect = {22, 0, 11, 16},
70 .offset = {10, 0},
71 },
72 };
73
74 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
75 {
76 float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
77 float xScale = flip ? -1.0f : 1.0f;
78 Camera3D camera = currentLevel->camera;
79 float size = 0.5f * unitScale;
80 // we want the sprite to face the camera, so we need to calculate the up vector
81 Vector3 forward = Vector3Subtract(camera.target, camera.position);
82 Vector3 up = {0, 1, 0};
83 Vector3 right = Vector3CrossProduct(forward, up);
84 up = Vector3Normalize(Vector3CrossProduct(right, forward));
85
86 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
87 {
88 SpriteAnimation anim = unit.animations[i];
89 if (anim.animationId != phase && anim.animationId != 0)
90 {
91 continue;
92 }
93 Rectangle srcRect = anim.srcRect;
94 if (anim.frameCount > 1)
95 {
96 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
97 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
98 }
99 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
100 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
101
102 if (flip)
103 {
104 srcRect.x += srcRect.width;
105 srcRect.width = -srcRect.width;
106 offset.x = scale.x - offset.x;
107 }
108 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
109 // move the sprite slightly towards the camera to avoid z-fighting
110 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
111 }
112 }
113
114 void TowerInit()
115 {
116 for (int i = 0; i < TOWER_MAX_COUNT; i++)
117 {
118 towers[i] = (Tower){0};
119 }
120 towerCount = 0;
121
122 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
123 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
124
125 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
126 {
127 if (towerModels[i].materials)
128 {
129 // assign the palette texture to the material of the model (0 is not used afaik)
130 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
131 }
132 }
133 }
134
135 static void TowerGunUpdate(Tower *tower)
136 {
137 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
138 if (tower->cooldown <= 0.0f)
139 {
140 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
141 if (enemy)
142 {
143 tower->cooldown = config.cooldown;
144 // shoot the enemy; determine future position of the enemy
145 float bulletSpeed = config.projectileSpeed;
146 Vector2 velocity = enemy->simVelocity;
147 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
148 Vector2 towerPosition = {tower->x, tower->y};
149 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
150 for (int i = 0; i < 8; i++) {
151 velocity = enemy->simVelocity;
152 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
153 float distance = Vector2Distance(towerPosition, futurePosition);
154 float eta2 = distance / bulletSpeed;
155 if (fabs(eta - eta2) < 0.01f) {
156 break;
157 }
158 eta = (eta2 + eta) * 0.5f;
159 }
160
161 ProjectileTryAdd(config.projectileType, enemy,
162 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
163 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
164 bulletSpeed, config.hitEffect);
165 enemy->futureDamage += config.hitEffect.damage;
166 tower->lastTargetPosition = futurePosition;
167 }
168 }
169 else
170 {
171 tower->cooldown -= gameTime.deltaTime;
172 }
173 }
174
175 Tower *TowerGetAt(int16_t x, int16_t y)
176 {
177 for (int i = 0; i < towerCount; i++)
178 {
179 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
180 {
181 return &towers[i];
182 }
183 }
184 return 0;
185 }
186
187 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
188 {
189 if (towerCount >= TOWER_MAX_COUNT)
190 {
191 return 0;
192 }
193
194 Tower *tower = TowerGetAt(x, y);
195 if (tower)
196 {
197 return 0;
198 }
199
200 tower = &towers[towerCount++];
201 tower->x = x;
202 tower->y = y;
203 tower->towerType = towerType;
204 tower->cooldown = 0.0f;
205 tower->damage = 0.0f;
206 return tower;
207 }
208
209 Tower *GetTowerByType(uint8_t towerType)
210 {
211 for (int i = 0; i < towerCount; i++)
212 {
213 if (towers[i].towerType == towerType)
214 {
215 return &towers[i];
216 }
217 }
218 return 0;
219 }
220
221 int GetTowerCosts(uint8_t towerType)
222 {
223 return towerTypeConfigs[towerType].cost;
224 }
225
226 float TowerGetMaxHealth(Tower *tower)
227 {
228 return towerTypeConfigs[tower->towerType].maxHealth;
229 }
230
231 void TowerDrawSingle(Tower tower)
232 {
233 if (tower.towerType == TOWER_TYPE_NONE)
234 {
235 return;
236 }
237
238 switch (tower.towerType)
239 {
240 case TOWER_TYPE_ARCHER:
241 {
242 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
243 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
244 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
245 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
246 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
247 }
248 break;
249 case TOWER_TYPE_BALLISTA:
250 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
251 break;
252 case TOWER_TYPE_CATAPULT:
253 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
254 break;
255 default:
256 if (towerModels[tower.towerType].materials)
257 {
258 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
259 } else {
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
261 }
262 break;
263 }
264 }
265
266 void TowerDraw()
267 {
268 for (int i = 0; i < towerCount; i++)
269 {
270 TowerDrawSingle(towers[i]);
271 }
272 }
273
274 void TowerUpdate()
275 {
276 for (int i = 0; i < towerCount; i++)
277 {
278 Tower *tower = &towers[i];
279 switch (tower->towerType)
280 {
281 case TOWER_TYPE_CATAPULT:
282 case TOWER_TYPE_BALLISTA:
283 case TOWER_TYPE_ARCHER:
284 TowerGunUpdate(tower);
285 break;
286 }
287 }
288 }
289
290 void TowerDrawHealthBars(Camera3D camera)
291 {
292 for (int i = 0; i < towerCount; i++)
293 {
294 Tower *tower = &towers[i];
295 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
296 {
297 continue;
298 }
299
300 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
301 float maxHealth = TowerGetMaxHealth(tower);
302 float health = maxHealth - tower->damage;
303 float healthRatio = health / maxHealth;
304
305 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
306 }
307 }
1 #include "td_main.h"
2 #include <raymath.h>
3
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 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 first wave uses now the shield enemy so it is easy to test, how it behaves. The shield is absorbing much of the damage and has quite some amount of hitpoints. The ballista is however dealing more damage than the shield can absorb, so the remaining damage is applied to the minion behind the shield. This way, the ballista can take out shielded enemies much quicker than the arrow tower. The additional health bar for the shield is also working as intended:
Conclusion
We added 3 different new types of enemies in this part of the tutorial. The shield enemy adds a first new flavor that tastes like rock-paper-scissors.
The catapult tower could play out its area damage against groups of enemies, but the current wave configuration does not allow for this.
In the next part, we'll add a tower upgrade system and refine the UI a bit.