Simple tower defense tutorial, part 17: Tower upgrading, 1/2
Upgrading towers is an important aspect in most tower defense games. There are plenty of mechanics like merge mechanics, science trees, items, etc.
For this part, I want to establish the foundation to allow upgrading towers by clicking on towers and selecting options from a context menu. We thus need to implement a context menu first.
When tapping on a free cell, we offer options to build a tower. The tower can still be moved around by dragging it, but the initial position is the cell where the context menu was opened. This way, we can simplify the UI.
When tapping on a tower, we offer options to upgrade, repair or sell the tower.
Let's start with the context menu for building towers:
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 = 11.5f;
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 int 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 return 1;
234 }
235 return 0;
236 }
237
238 float GetRandomFloat(float min, float max)
239 {
240 int random = GetRandomValue(0, 0xfffffff);
241 return ((float)random / (float)0xfffffff) * (max - min) + min;
242 }
243
244 void DrawLevelGround(Level *level)
245 {
246 // draw checkerboard ground pattern
247 for (int x = -5; x <= 5; x += 1)
248 {
249 for (int y = -5; y <= 5; y += 1)
250 {
251 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
252 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
253 }
254 }
255
256 int oldSeed = GetRandomValue(0, 0xfffffff);
257 SetRandomSeed(level->seed);
258 // increase probability for trees via duplicated entries
259 Model borderModels[64];
260 int maxRockCount = GetRandomValue(2, 6);
261 int maxTreeCount = GetRandomValue(10, 20);
262 int maxFirTreeCount = GetRandomValue(5, 10);
263 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
264 int grassPatchCount = GetRandomValue(5, 30);
265
266 int modelCount = 0;
267 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
268 {
269 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
270 }
271 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
272 {
273 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
274 }
275 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
276 {
277 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
278 }
279 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
280 {
281 borderModels[modelCount++] = grassPatchModel[0];
282 }
283
284 // draw some objects around the border of the map
285 Vector3 up = {0, 1, 0};
286 // a pseudo random number generator to get the same result every time
287 const float wiggle = 0.75f;
288 const int layerCount = 3;
289 for (int layer = 0; layer <= layerCount; layer++)
290 {
291 int layerPos = 6 + layer;
292 Model *selectedModels = borderModels;
293 int selectedModelCount = modelCount;
294 if (layer == 0)
295 {
296 selectedModels = grassPatchModel;
297 selectedModelCount = 1;
298 }
299 for (int x = -6 + layer; x <= 6 + layer; x += 1)
300 {
301 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
302 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
303 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
304 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
305 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
306 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
307 }
308
309 for (int z = -5 + layer; z <= 5 + layer; z += 1)
310 {
311 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
312 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
313 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
314 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
315 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
316 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
317 }
318 }
319
320 SetRandomSeed(oldSeed);
321 }
322
323 void DrawEnemyPath(Level *level, Color arrowColor)
324 {
325 const int castleX = 0, castleY = 0;
326 const int maxWaypointCount = 200;
327 const float timeStep = 1.0f;
328 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
329
330 // we start with a time offset to simulate the path,
331 // this way the arrows are animated in a forward moving direction
332 // The time is wrapped around the time step to get a smooth animation
333 float timeOffset = fmodf(GetTime(), timeStep);
334
335 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
336 {
337 EnemyWave *wave = &level->waves[i];
338 if (wave->wave != level->currentWave)
339 {
340 continue;
341 }
342
343 // use this dummy enemy to simulate the path
344 Enemy dummy = {
345 .enemyType = ENEMY_TYPE_MINION,
346 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
347 .nextX = wave->spawnPosition.x,
348 .nextY = wave->spawnPosition.y,
349 .currentX = wave->spawnPosition.x,
350 .currentY = wave->spawnPosition.y,
351 };
352
353 float deltaTime = timeOffset;
354 for (int j = 0; j < maxWaypointCount; j++)
355 {
356 int waypointPassedCount = 0;
357 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
358 // after the initial variable starting offset, we use a fixed time step
359 deltaTime = timeStep;
360 dummy.simPosition = pos;
361
362 // Update the dummy's position just like we do in the regular enemy update loop
363 for (int k = 0; k < waypointPassedCount; k++)
364 {
365 dummy.currentX = dummy.nextX;
366 dummy.currentY = dummy.nextY;
367 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
368 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
369 {
370 break;
371 }
372 }
373 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
374 {
375 break;
376 }
377
378 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
379 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
380 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
381 }
382 }
383 }
384
385 void DrawEnemyPaths(Level *level)
386 {
387 // disable depth testing for the path arrows
388 // flush the 3D batch to draw the arrows on top of everything
389 rlDrawRenderBatchActive();
390 rlDisableDepthTest();
391 DrawEnemyPath(level, (Color){64, 64, 64, 160});
392
393 rlDrawRenderBatchActive();
394 rlEnableDepthTest();
395 DrawEnemyPath(level, WHITE);
396 }
397
398 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
399 {
400 float dt = gameTime.fixedDeltaTime;
401 // smooth transition for the placement position using exponential decay
402 const float lambda = 15.0f;
403 float factor = 1.0f - expf(-lambda * dt);
404
405 float damping = 0.5f;
406 float springStiffness = 300.0f;
407 float springDecay = 95.0f;
408 float minHeight = 0.35f;
409
410 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
411 {
412 damping = 1.0f;
413 springDecay = 90.0f;
414 springStiffness = 100.0f;
415 minHeight = 0.70f;
416 }
417
418 for (int i = 0; i < gameTime.fixedStepCount; i++)
419 {
420 level->placementTransitionPosition =
421 Vector2Lerp(
422 level->placementTransitionPosition,
423 (Vector2){mapX, mapY}, factor);
424
425 // draw the spring position for debugging the spring simulation
426 // first step: stiff spring, no simulation
427 Vector3 worldPlacementPosition = (Vector3){
428 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
429 Vector3 springTargetPosition = (Vector3){
430 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
431 // consider the current velocity to predict the future position in order to dampen
432 // the spring simulation. Longer prediction times will result in more damping
433 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
434 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
435 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
436 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
437 // decay velocity of the upright forcing spring
438 // This force acts like a 2nd spring that pulls the tip upright into the air above the
439 // base position
440 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
441 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
442
443 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
444 // we use a simple spring model with a rest length of 1.0f
445 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
446 float springLength = Vector3Length(springDelta);
447 float springForce = (springLength - 1.0f) * springStiffness;
448 Vector3 springForceVector = Vector3Normalize(springDelta);
449 springForceVector = Vector3Scale(springForceVector, springForce);
450 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
451 Vector3Scale(springForceVector, dt));
452
453 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
454 Vector3Scale(level->placementTowerSpring.velocity, dt));
455 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
456 {
457 level->placementTowerSpring.velocity.y *= -1.0f;
458 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
459 }
460 }
461 }
462
463 void DrawLevelBuildingPlacementState(Level *level)
464 {
465 const float placementDuration = 0.5f;
466
467 level->placementTimer += gameTime.deltaTime;
468 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
469 {
470 level->placementPhase = PLACEMENT_PHASE_MOVING;
471 level->placementTimer = 0.0f;
472 }
473
474 BeginMode3D(level->camera);
475 DrawLevelGround(level);
476
477 int blockedCellCount = 0;
478 Vector2 blockedCells[1];
479 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
480 float planeDistance = ray.position.y / -ray.direction.y;
481 float planeX = ray.direction.x * planeDistance + ray.position.x;
482 float planeY = ray.direction.z * planeDistance + ray.position.z;
483 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
484 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
485 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
486 level->placementMode && !guiState.isBlocked &&
487 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
488 {
489 level->placementX = mapX;
490 level->placementY = mapY;
491 }
492 else
493 {
494 mapX = level->placementX;
495 mapY = level->placementY;
496 }
497 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
498 PathFindingMapUpdate(blockedCellCount, blockedCells);
499
500 TowerDraw();
501 EnemyDraw();
502 ProjectileDraw();
503 ParticleDraw();
504 DrawEnemyPaths(level);
505
506 // let the tower float up and down. Consider this height in the spring simulation as well
507 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
508
509 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
510 {
511 // The bouncing spring needs a bit of outro time to look nice and complete.
512 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
513 // and the last 1/3rd is the outro physics (bouncing)
514 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
515 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
516 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
517 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
518 towerFloatHeight = linearBlendHeight + parabola;
519 }
520
521 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
522
523 rlPushMatrix();
524 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
525
526 rlPushMatrix();
527 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
528 // calculate x and z rotation to align the model with the spring
529 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
530 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
531 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
532 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
533 float springLength = Vector3Length(towerUp);
534 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
535 float towerSquash = 1.0f / towerStretch;
536 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
537 rlScalef(towerSquash, towerStretch, towerSquash);
538 Tower dummy = {
539 .towerType = level->placementMode,
540 };
541 TowerDrawSingle(dummy);
542 rlPopMatrix();
543
544 // draw a shadow for the tower
545 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
546 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
547 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
548
549
550 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
551 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
552 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
553 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
554
555 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
556 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
557 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
558 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
559 rlPopMatrix();
560
561 guiState.isBlocked = 0;
562
563 EndMode3D();
564
565 TowerDrawHealthBars(level->camera);
566
567 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
568 {
569 if (level->placementTimer > placementDuration)
570 {
571 TowerTryAdd(level->placementMode, mapX, mapY);
572 level->playerGold -= GetTowerCosts(level->placementMode);
573 level->nextState = LEVEL_STATE_BUILDING;
574 level->placementMode = TOWER_TYPE_NONE;
575 }
576 }
577 else
578 {
579 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
580 {
581 level->nextState = LEVEL_STATE_BUILDING;
582 level->placementMode = TOWER_TYPE_NONE;
583 TraceLog(LOG_INFO, "Cancel building");
584 }
585
586 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
587 {
588 level->placementPhase = PLACEMENT_PHASE_PLACING;
589 level->placementTimer = 0.0f;
590 }
591 }
592 }
593
594 void DrawLevelBuildingState(Level *level)
595 {
596 BeginMode3D(level->camera);
597 DrawLevelGround(level);
598
599 PathFindingMapUpdate(0, 0);
600 TowerDraw();
601 EnemyDraw();
602 ProjectileDraw();
603 ParticleDraw();
604 DrawEnemyPaths(level);
605
606 guiState.isBlocked = 0;
607
608 // when the context menu is not active, we update the placement position
609 if (level->placementContextMenuStatus == 0)
610 {
611 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
612 float hitDistance = ray.position.y / -ray.direction.y;
613 float hitX = ray.direction.x * hitDistance + ray.position.x;
614 float hitY = ray.direction.z * hitDistance + ray.position.z;
615 level->placementX = (int)floorf(hitX + 0.5f);
616 level->placementY = (int)floorf(hitY + 0.5f);
617 }
618
619 // Hover rectangle, when the mouse is over the map
620 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
621 if (isHovering)
622 {
623 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
624 }
625
626 EndMode3D();
627
628 TowerDrawHealthBars(level->camera);
629
630 // Draw the context menu when the context menu is active
631 if (level->placementContextMenuStatus >= 1)
632 {
633 // The context menu can open above or below the placement position
634 // Find both points and find out which position we want to use
635 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
636 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
637 Rectangle contextMenu = {0, 0, 130, 145};
638 if (anchorHigh.y > contextMenu.height) {
639 // context menu is above the placement position, which should be default
640 contextMenu.y = anchorHigh.y - contextMenu.height;
641 }
642 else {
643 // context menu is below the placement position
644 contextMenu.y = anchorLow.y;
645 }
646 // center the context menu, respecting the limits
647 contextMenu.x = anchorLow.x - contextMenu.width * 0.5f;
648 if (contextMenu.x < 0) {
649 contextMenu.x = 0;
650 }
651 if (contextMenu.x + contextMenu.width > GetScreenWidth()) {
652 contextMenu.x = GetScreenWidth() - contextMenu.width;
653 }
654
655 // handle closing the context menu
656 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
657 {
658 level->placementContextMenuStatus = 0;
659 }
660
661 DrawRectangle(contextMenu.x, contextMenu.y, contextMenu.width, contextMenu.height, (Color){0, 0, 0, 128});
662 Tower *tower = TowerGetAt(level->placementX, level->placementY);
663 if (tower == 0)
664 {
665 if (
666 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 5, contextMenu.width - 10, 30, TOWER_TYPE_WALL, "Wall") ||
667 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 40, contextMenu.width - 10, 30, TOWER_TYPE_ARCHER, "Archer") ||
668 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 75, contextMenu.width - 10, 30, TOWER_TYPE_BALLISTA, "Ballista") ||
669 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 110, contextMenu.width - 10, 30, TOWER_TYPE_CATAPULT, "Catapult"))
670 {
671 level->placementContextMenuStatus = 0;
672 }
673 }
674 else
675 {
676
677 }
678 }
679 // Activate the context menu when the mouse is clicked and the context menu is not active
680 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus == 0)
681 {
682 level->placementContextMenuStatus = 1;
683 }
684
685
686 if (level->placementContextMenuStatus == 0)
687 {
688 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
689 {
690 level->nextState = LEVEL_STATE_RESET;
691 }
692
693 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
694 {
695 level->nextState = LEVEL_STATE_BATTLE;
696 }
697
698 const char *text = "Building phase";
699 int textWidth = MeasureText(text, 20);
700 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 10, 20, WHITE);
701 }
702
703 }
704
705 void InitBattleStateConditions(Level *level)
706 {
707 level->state = LEVEL_STATE_BATTLE;
708 level->nextState = LEVEL_STATE_NONE;
709 level->waveEndTimer = 0.0f;
710 for (int i = 0; i < 10; i++)
711 {
712 EnemyWave *wave = &level->waves[i];
713 wave->spawned = 0;
714 wave->timeToSpawnNext = wave->delay;
715 }
716 }
717
718 void DrawLevelBattleState(Level *level)
719 {
720 BeginMode3D(level->camera);
721 DrawLevelGround(level);
722 TowerDraw();
723 EnemyDraw();
724 ProjectileDraw();
725 ParticleDraw();
726 guiState.isBlocked = 0;
727 EndMode3D();
728
729 EnemyDrawHealthbars(level->camera);
730 TowerDrawHealthBars(level->camera);
731
732 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
733 {
734 level->nextState = LEVEL_STATE_RESET;
735 }
736
737 int maxCount = 0;
738 int remainingCount = 0;
739 for (int i = 0; i < 10; i++)
740 {
741 EnemyWave *wave = &level->waves[i];
742 if (wave->wave != level->currentWave)
743 {
744 continue;
745 }
746 maxCount += wave->count;
747 remainingCount += wave->count - wave->spawned;
748 }
749 int aliveCount = EnemyCount();
750 remainingCount += aliveCount;
751
752 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
753 int textWidth = MeasureText(text, 20);
754 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
755 }
756
757 void DrawLevel(Level *level)
758 {
759 switch (level->state)
760 {
761 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
762 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
763 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
764 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
765 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
766 default: break;
767 }
768
769 DrawLevelHud(level);
770 }
771
772 void UpdateLevel(Level *level)
773 {
774 if (level->state == LEVEL_STATE_BATTLE)
775 {
776 int activeWaves = 0;
777 for (int i = 0; i < 10; i++)
778 {
779 EnemyWave *wave = &level->waves[i];
780 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
781 {
782 continue;
783 }
784 activeWaves++;
785 wave->timeToSpawnNext -= gameTime.deltaTime;
786 if (wave->timeToSpawnNext <= 0.0f)
787 {
788 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
789 if (enemy)
790 {
791 wave->timeToSpawnNext = wave->interval;
792 wave->spawned++;
793 }
794 }
795 }
796 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
797 level->waveEndTimer += gameTime.deltaTime;
798 if (level->waveEndTimer >= 2.0f)
799 {
800 level->nextState = LEVEL_STATE_LOST_WAVE;
801 }
802 }
803 else if (activeWaves == 0 && EnemyCount() == 0)
804 {
805 level->waveEndTimer += gameTime.deltaTime;
806 if (level->waveEndTimer >= 2.0f)
807 {
808 level->nextState = LEVEL_STATE_WON_WAVE;
809 }
810 }
811 }
812
813 PathFindingMapUpdate(0, 0);
814 EnemyUpdate();
815 TowerUpdate();
816 ProjectileUpdate();
817 ParticleUpdate();
818
819 if (level->nextState == LEVEL_STATE_RESET)
820 {
821 InitLevel(level);
822 }
823
824 if (level->nextState == LEVEL_STATE_BATTLE)
825 {
826 InitBattleStateConditions(level);
827 }
828
829 if (level->nextState == LEVEL_STATE_WON_WAVE)
830 {
831 level->currentWave++;
832 level->state = LEVEL_STATE_WON_WAVE;
833 }
834
835 if (level->nextState == LEVEL_STATE_LOST_WAVE)
836 {
837 level->state = LEVEL_STATE_LOST_WAVE;
838 }
839
840 if (level->nextState == LEVEL_STATE_BUILDING)
841 {
842 level->state = LEVEL_STATE_BUILDING;
843 }
844
845 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
846 {
847 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
848 level->placementTransitionPosition = (Vector2){
849 level->placementX, level->placementY};
850 // initialize the spring to the current position
851 level->placementTowerSpring = (PhysicsPoint){
852 .position = (Vector3){level->placementX, 8.0f, level->placementY},
853 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
854 };
855 level->placementPhase = PLACEMENT_PHASE_STARTING;
856 level->placementTimer = 0.0f;
857 }
858
859 if (level->nextState == LEVEL_STATE_WON_LEVEL)
860 {
861 // make something of this later
862 InitLevel(level);
863 }
864
865 level->nextState = LEVEL_STATE_NONE;
866 }
867
868 float nextSpawnTime = 0.0f;
869
870 void ResetGame()
871 {
872 InitLevel(currentLevel);
873 }
874
875 void InitGame()
876 {
877 TowerInit();
878 EnemyInit();
879 ProjectileInit();
880 ParticleInit();
881 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
882
883 currentLevel = levels;
884 InitLevel(currentLevel);
885 }
886
887 //# Immediate GUI functions
888
889 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
890 {
891 const float healthBarHeight = 6.0f;
892 const float healthBarOffset = 15.0f;
893 const float inset = 2.0f;
894 const float innerWidth = healthBarWidth - inset * 2;
895 const float innerHeight = healthBarHeight - inset * 2;
896
897 Vector2 screenPos = GetWorldToScreen(position, camera);
898 screenPos = Vector2Add(screenPos, screenOffset);
899 float centerX = screenPos.x - healthBarWidth * 0.5f;
900 float topY = screenPos.y - healthBarOffset;
901 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
902 float healthWidth = innerWidth * healthRatio;
903 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
904 }
905
906 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
907 {
908 Rectangle bounds = {x, y, width, height};
909 int isPressed = 0;
910 int isSelected = state && state->isSelected;
911 int isDisabled = state && state->isDisabled;
912 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
913 {
914 Color color = isSelected ? DARKGRAY : GRAY;
915 DrawRectangle(x, y, width, height, color);
916 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
917 {
918 isPressed = 1;
919 }
920 guiState.isBlocked = 1;
921 }
922 else
923 {
924 Color color = isSelected ? WHITE : LIGHTGRAY;
925 DrawRectangle(x, y, width, height, color);
926 }
927 Font font = GetFontDefault();
928 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
929 Color textColor = isDisabled ? GRAY : BLACK;
930 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
931 return isPressed;
932 }
933
934 //# Main game loop
935
936 void GameUpdate()
937 {
938 UpdateLevel(currentLevel);
939 }
940
941 int main(void)
942 {
943 int screenWidth, screenHeight;
944 GetPreferredSize(&screenWidth, &screenHeight);
945 InitWindow(screenWidth, screenHeight, "Tower defense");
946 float gamespeed = 1.0f;
947 SetTargetFPS(30);
948
949 LoadAssets();
950 InitGame();
951
952 float pause = 1.0f;
953
954 while (!WindowShouldClose())
955 {
956 if (IsPaused()) {
957 // canvas is not visible in browser - do nothing
958 continue;
959 }
960
961 if (IsKeyPressed(KEY_T))
962 {
963 gamespeed += 0.1f;
964 if (gamespeed > 1.05f) gamespeed = 0.1f;
965 }
966
967 if (IsKeyPressed(KEY_P))
968 {
969 pause = pause > 0.5f ? 0.0f : 1.0f;
970 }
971
972 float dt = GetFrameTime() * gamespeed * pause;
973 // cap maximum delta time to 0.1 seconds to prevent large time steps
974 if (dt > 0.1f) dt = 0.1f;
975 gameTime.time += dt;
976 gameTime.deltaTime = dt;
977 gameTime.frameCount += 1;
978
979 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
980 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
981
982 BeginDrawing();
983 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
984
985 GameUpdate();
986 DrawLevel(currentLevel);
987
988 if (gamespeed != 1.0f)
989 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
990 EndDrawing();
991
992 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
993 }
994
995 CloseWindow();
996
997 return 0;
998 }
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
148 int16_t placementX;
149 int16_t placementY;
150 int8_t placementContextMenuStatus;
151
152 Vector2 placementTransitionPosition;
153 PhysicsPoint placementTowerSpring;
154
155 int initialGold;
156 int playerGold;
157
158 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
159 int currentWave;
160 float waveEndTimer;
161 } Level;
162
163 typedef struct DeltaSrc
164 {
165 char x, y;
166 } DeltaSrc;
167
168 typedef struct PathfindingMap
169 {
170 int width, height;
171 float scale;
172 float *distances;
173 long *towerIndex;
174 DeltaSrc *deltaSrc;
175 float maxDistance;
176 Matrix toMapSpace;
177 Matrix toWorldSpace;
178 } PathfindingMap;
179
180 // when we execute the pathfinding algorithm, we need to store the active nodes
181 // in a queue. Each node has a position, a distance from the start, and the
182 // position of the node that we came from.
183 typedef struct PathfindingNode
184 {
185 int16_t x, y, fromX, fromY;
186 float distance;
187 } PathfindingNode;
188
189 typedef struct EnemyId
190 {
191 uint16_t index;
192 uint16_t generation;
193 } EnemyId;
194
195 typedef struct EnemyClassConfig
196 {
197 float speed;
198 float health;
199 float shieldHealth;
200 float shieldDamageAbsorption;
201 float radius;
202 float maxAcceleration;
203 float requiredContactTime;
204 float explosionDamage;
205 float explosionRange;
206 float explosionPushbackPower;
207 int goldValue;
208 } EnemyClassConfig;
209
210 typedef struct Enemy
211 {
212 int16_t currentX, currentY;
213 int16_t nextX, nextY;
214 Vector2 simPosition;
215 Vector2 simVelocity;
216 uint16_t generation;
217 float walkedDistance;
218 float startMovingTime;
219 float damage, futureDamage;
220 float shieldDamage;
221 float contactTime;
222 uint8_t enemyType;
223 uint8_t movePathCount;
224 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
225 } Enemy;
226
227 // a unit that uses sprites to be drawn
228 #define SPRITE_UNIT_ANIMATION_COUNT 6
229 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
230 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
231 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
232
233 typedef struct SpriteAnimation
234 {
235 Rectangle srcRect;
236 Vector2 offset;
237 uint8_t animationId;
238 uint8_t frameCount;
239 uint8_t frameWidth;
240 float frameDuration;
241 } SpriteAnimation;
242
243 typedef struct SpriteUnit
244 {
245 float scale;
246 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
247 } SpriteUnit;
248
249 #define PROJECTILE_MAX_COUNT 1200
250 #define PROJECTILE_TYPE_NONE 0
251 #define PROJECTILE_TYPE_ARROW 1
252 #define PROJECTILE_TYPE_CATAPULT 2
253 #define PROJECTILE_TYPE_BALLISTA 3
254
255 typedef struct Projectile
256 {
257 uint8_t projectileType;
258 float shootTime;
259 float arrivalTime;
260 float distance;
261 Vector3 position;
262 Vector3 target;
263 Vector3 directionNormal;
264 EnemyId targetEnemy;
265 HitEffectConfig hitEffectConfig;
266 } Projectile;
267
268 //# Function declarations
269 float TowerGetMaxHealth(Tower *tower);
270 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
271 int EnemyAddDamageRange(Vector2 position, float range, float damage);
272 int EnemyAddDamage(Enemy *enemy, float damage);
273
274 //# Enemy functions
275 void EnemyInit();
276 void EnemyDraw();
277 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
278 void EnemyUpdate();
279 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
280 float EnemyGetMaxHealth(Enemy *enemy);
281 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
282 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
283 EnemyId EnemyGetId(Enemy *enemy);
284 Enemy *EnemyTryResolve(EnemyId enemyId);
285 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
286 int EnemyAddDamage(Enemy *enemy, float damage);
287 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
288 int EnemyCount();
289 void EnemyDrawHealthbars(Camera3D camera);
290
291 //# Tower functions
292 void TowerInit();
293 Tower *TowerGetAt(int16_t x, int16_t y);
294 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
295 Tower *GetTowerByType(uint8_t towerType);
296 int GetTowerCosts(uint8_t towerType);
297 float TowerGetMaxHealth(Tower *tower);
298 void TowerDraw();
299 void TowerDrawSingle(Tower tower);
300 void TowerUpdate();
301 void TowerDrawHealthBars(Camera3D camera);
302 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
303
304 //# Particles
305 void ParticleInit();
306 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
307 void ParticleUpdate();
308 void ParticleDraw();
309
310 //# Projectiles
311 void ProjectileInit();
312 void ProjectileDraw();
313 void ProjectileUpdate();
314 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
315
316 //# Pathfinding map
317 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
318 float PathFindingGetDistance(int mapX, int mapY);
319 Vector2 PathFindingGetGradient(Vector3 world);
320 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
321 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
322 void PathFindingMapDraw();
323
324 //# UI
325 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
326
327 //# Level
328 void DrawLevelGround(Level *level);
329 void DrawEnemyPath(Level *level, Color arrowColor);
330
331 //# variables
332 extern Level *currentLevel;
333 extern Enemy enemies[ENEMY_MAX_COUNT];
334 extern int enemyCount;
335 extern EnemyClassConfig enemyClassConfigs[];
336
337 extern GUIState guiState;
338 extern GameTime gameTime;
339 extern Tower towers[TOWER_MAX_COUNT];
340 extern int towerCount;
341
342 extern Texture2D palette, spriteSheet;
343
344 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 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 #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 #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
That was not too hard. The most complex part is to handle the mouse inputs: When changing the state of the context menu ( level->placementContextMenuStatus ), we have to do that in the right order. This would not be necessary if we had a nextStatus field that is used to transition - a reminder that mutability makes things more complicated. I might change that in the future in case this becomes more complicated.
So far, the implementation is still quite simple though, considering that there is no UI framework at play, just a few rectangles and text. I would for now continue this way. At a later point, we want to improve the design, maybe it makes then sense to formalize a few things.
The next step is to trigger a different menu when clicking on a tower. For the castle at 0,0, we currently have nothing to offer, so let's just show some stats.
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 = 1,
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 = 0.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 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
75
76 //# Game
77
78 static Model LoadGLBModel(char *filename)
79 {
80 Model model = LoadModel(TextFormat("data/%s.glb",filename));
81 for (int i = 0; i < model.materialCount; i++)
82 {
83 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
84 }
85 return model;
86 }
87
88 void LoadAssets()
89 {
90 // load a sprite sheet that contains all units
91 spriteSheet = LoadTexture("data/spritesheet.png");
92 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
93
94 // we'll use a palette texture to colorize the all buildings and environment art
95 palette = LoadTexture("data/palette.png");
96 // The texture uses gradients on very small space, so we'll enable bilinear filtering
97 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
98
99 floorTileAModel = LoadGLBModel("floor-tile-a");
100 floorTileBModel = LoadGLBModel("floor-tile-b");
101 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
102 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
103 firTreeModel[0] = LoadGLBModel("firtree-1-a");
104 firTreeModel[1] = LoadGLBModel("firtree-1-b");
105 rockModels[0] = LoadGLBModel("rock-1");
106 rockModels[1] = LoadGLBModel("rock-2");
107 rockModels[2] = LoadGLBModel("rock-3");
108 rockModels[3] = LoadGLBModel("rock-4");
109 rockModels[4] = LoadGLBModel("rock-5");
110 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
111
112 pathArrowModel = LoadGLBModel("direction-arrow-x");
113 greenArrowModel = LoadGLBModel("green-arrow");
114 }
115
116 void InitLevel(Level *level)
117 {
118 level->seed = (int)(GetTime() * 100.0f);
119
120 TowerInit();
121 EnemyInit();
122 ProjectileInit();
123 ParticleInit();
124 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
125
126 level->placementMode = 0;
127 level->state = LEVEL_STATE_BUILDING;
128 level->nextState = LEVEL_STATE_NONE;
129 level->playerGold = level->initialGold;
130 level->currentWave = 0;
131 level->placementX = -1;
132 level->placementY = 0;
133
134 Camera *camera = &level->camera;
135 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
136 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
137 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
138 camera->fovy = 11.5f;
139 camera->projection = CAMERA_ORTHOGRAPHIC;
140 }
141
142 void DrawLevelHud(Level *level)
143 {
144 const char *text = TextFormat("Gold: %d", level->playerGold);
145 Font font = GetFontDefault();
146 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
147 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
148 }
149
150 void DrawLevelReportLostWave(Level *level)
151 {
152 BeginMode3D(level->camera);
153 DrawLevelGround(level);
154 TowerDraw();
155 EnemyDraw();
156 ProjectileDraw();
157 ParticleDraw();
158 guiState.isBlocked = 0;
159 EndMode3D();
160
161 TowerDrawHealthBars(level->camera);
162
163 const char *text = "Wave lost";
164 int textWidth = MeasureText(text, 20);
165 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
166
167 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
168 {
169 level->nextState = LEVEL_STATE_RESET;
170 }
171 }
172
173 int HasLevelNextWave(Level *level)
174 {
175 for (int i = 0; i < 10; i++)
176 {
177 EnemyWave *wave = &level->waves[i];
178 if (wave->wave == level->currentWave)
179 {
180 return 1;
181 }
182 }
183 return 0;
184 }
185
186 void DrawLevelReportWonWave(Level *level)
187 {
188 BeginMode3D(level->camera);
189 DrawLevelGround(level);
190 TowerDraw();
191 EnemyDraw();
192 ProjectileDraw();
193 ParticleDraw();
194 guiState.isBlocked = 0;
195 EndMode3D();
196
197 TowerDrawHealthBars(level->camera);
198
199 const char *text = "Wave won";
200 int textWidth = MeasureText(text, 20);
201 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
202
203
204 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
205 {
206 level->nextState = LEVEL_STATE_RESET;
207 }
208
209 if (HasLevelNextWave(level))
210 {
211 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
212 {
213 level->nextState = LEVEL_STATE_BUILDING;
214 }
215 }
216 else {
217 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
218 {
219 level->nextState = LEVEL_STATE_WON_LEVEL;
220 }
221 }
222 }
223
224 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
225 {
226 static ButtonState buttonStates[8] = {0};
227 int cost = GetTowerCosts(towerType);
228 const char *text = TextFormat("%s: %d", name, cost);
229 buttonStates[towerType].isSelected = level->placementMode == towerType;
230 buttonStates[towerType].isDisabled = level->playerGold < cost;
231 if (Button(text, x, y, width, height, &buttonStates[towerType]))
232 {
233 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
234 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
235 return 1;
236 }
237 return 0;
238 }
239
240 float GetRandomFloat(float min, float max)
241 {
242 int random = GetRandomValue(0, 0xfffffff);
243 return ((float)random / (float)0xfffffff) * (max - min) + min;
244 }
245
246 void DrawLevelGround(Level *level)
247 {
248 // draw checkerboard ground pattern
249 for (int x = -5; x <= 5; x += 1)
250 {
251 for (int y = -5; y <= 5; y += 1)
252 {
253 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
254 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
255 }
256 }
257
258 int oldSeed = GetRandomValue(0, 0xfffffff);
259 SetRandomSeed(level->seed);
260 // increase probability for trees via duplicated entries
261 Model borderModels[64];
262 int maxRockCount = GetRandomValue(2, 6);
263 int maxTreeCount = GetRandomValue(10, 20);
264 int maxFirTreeCount = GetRandomValue(5, 10);
265 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
266 int grassPatchCount = GetRandomValue(5, 30);
267
268 int modelCount = 0;
269 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
270 {
271 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
272 }
273 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
274 {
275 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
276 }
277 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
278 {
279 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
280 }
281 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
282 {
283 borderModels[modelCount++] = grassPatchModel[0];
284 }
285
286 // draw some objects around the border of the map
287 Vector3 up = {0, 1, 0};
288 // a pseudo random number generator to get the same result every time
289 const float wiggle = 0.75f;
290 const int layerCount = 3;
291 for (int layer = 0; layer <= layerCount; layer++)
292 {
293 int layerPos = 6 + layer;
294 Model *selectedModels = borderModels;
295 int selectedModelCount = modelCount;
296 if (layer == 0)
297 {
298 selectedModels = grassPatchModel;
299 selectedModelCount = 1;
300 }
301 for (int x = -6 - layer; x <= 6 + layer; x += 1)
302 {
303 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
304 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
305 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
306 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
307 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
308 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
309 }
310
311 for (int z = -5 - layer; z <= 5 + layer; z += 1)
312 {
313 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
314 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
315 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
316 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
317 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
318 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
319 }
320 }
321
322 SetRandomSeed(oldSeed);
323 }
324
325 void DrawEnemyPath(Level *level, Color arrowColor)
326 {
327 const int castleX = 0, castleY = 0;
328 const int maxWaypointCount = 200;
329 const float timeStep = 1.0f;
330 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
331
332 // we start with a time offset to simulate the path,
333 // this way the arrows are animated in a forward moving direction
334 // The time is wrapped around the time step to get a smooth animation
335 float timeOffset = fmodf(GetTime(), timeStep);
336
337 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
338 {
339 EnemyWave *wave = &level->waves[i];
340 if (wave->wave != level->currentWave)
341 {
342 continue;
343 }
344
345 // use this dummy enemy to simulate the path
346 Enemy dummy = {
347 .enemyType = ENEMY_TYPE_MINION,
348 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
349 .nextX = wave->spawnPosition.x,
350 .nextY = wave->spawnPosition.y,
351 .currentX = wave->spawnPosition.x,
352 .currentY = wave->spawnPosition.y,
353 };
354
355 float deltaTime = timeOffset;
356 for (int j = 0; j < maxWaypointCount; j++)
357 {
358 int waypointPassedCount = 0;
359 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
360 // after the initial variable starting offset, we use a fixed time step
361 deltaTime = timeStep;
362 dummy.simPosition = pos;
363
364 // Update the dummy's position just like we do in the regular enemy update loop
365 for (int k = 0; k < waypointPassedCount; k++)
366 {
367 dummy.currentX = dummy.nextX;
368 dummy.currentY = dummy.nextY;
369 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
370 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
371 {
372 break;
373 }
374 }
375 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
376 {
377 break;
378 }
379
380 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
381 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
382 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
383 }
384 }
385 }
386
387 void DrawEnemyPaths(Level *level)
388 {
389 // disable depth testing for the path arrows
390 // flush the 3D batch to draw the arrows on top of everything
391 rlDrawRenderBatchActive();
392 rlDisableDepthTest();
393 DrawEnemyPath(level, (Color){64, 64, 64, 160});
394
395 rlDrawRenderBatchActive();
396 rlEnableDepthTest();
397 DrawEnemyPath(level, WHITE);
398 }
399
400 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
401 {
402 float dt = gameTime.fixedDeltaTime;
403 // smooth transition for the placement position using exponential decay
404 const float lambda = 15.0f;
405 float factor = 1.0f - expf(-lambda * dt);
406
407 float damping = 0.5f;
408 float springStiffness = 300.0f;
409 float springDecay = 95.0f;
410 float minHeight = 0.35f;
411
412 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
413 {
414 damping = 1.0f;
415 springDecay = 90.0f;
416 springStiffness = 100.0f;
417 minHeight = 0.70f;
418 }
419
420 for (int i = 0; i < gameTime.fixedStepCount; i++)
421 {
422 level->placementTransitionPosition =
423 Vector2Lerp(
424 level->placementTransitionPosition,
425 (Vector2){mapX, mapY}, factor);
426
427 // draw the spring position for debugging the spring simulation
428 // first step: stiff spring, no simulation
429 Vector3 worldPlacementPosition = (Vector3){
430 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
431 Vector3 springTargetPosition = (Vector3){
432 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
433 // consider the current velocity to predict the future position in order to dampen
434 // the spring simulation. Longer prediction times will result in more damping
435 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
436 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
437 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
438 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
439 // decay velocity of the upright forcing spring
440 // This force acts like a 2nd spring that pulls the tip upright into the air above the
441 // base position
442 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
443 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
444
445 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
446 // we use a simple spring model with a rest length of 1.0f
447 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
448 float springLength = Vector3Length(springDelta);
449 float springForce = (springLength - 1.0f) * springStiffness;
450 Vector3 springForceVector = Vector3Normalize(springDelta);
451 springForceVector = Vector3Scale(springForceVector, springForce);
452 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
453 Vector3Scale(springForceVector, dt));
454
455 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
456 Vector3Scale(level->placementTowerSpring.velocity, dt));
457 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
458 {
459 level->placementTowerSpring.velocity.y *= -1.0f;
460 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
461 }
462 }
463 }
464
465 void DrawLevelBuildingPlacementState(Level *level)
466 {
467 const float placementDuration = 0.5f;
468
469 level->placementTimer += gameTime.deltaTime;
470 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
471 {
472 level->placementPhase = PLACEMENT_PHASE_MOVING;
473 level->placementTimer = 0.0f;
474 }
475
476 BeginMode3D(level->camera);
477 DrawLevelGround(level);
478
479 int blockedCellCount = 0;
480 Vector2 blockedCells[1];
481 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
482 float planeDistance = ray.position.y / -ray.direction.y;
483 float planeX = ray.direction.x * planeDistance + ray.position.x;
484 float planeY = ray.direction.z * planeDistance + ray.position.z;
485 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
486 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
487 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
488 level->placementMode && !guiState.isBlocked &&
489 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
490 {
491 level->placementX = mapX;
492 level->placementY = mapY;
493 }
494 else
495 {
496 mapX = level->placementX;
497 mapY = level->placementY;
498 }
499 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
500 PathFindingMapUpdate(blockedCellCount, blockedCells);
501
502 TowerDraw();
503 EnemyDraw();
504 ProjectileDraw();
505 ParticleDraw();
506 DrawEnemyPaths(level);
507
508 // let the tower float up and down. Consider this height in the spring simulation as well
509 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
510
511 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
512 {
513 // The bouncing spring needs a bit of outro time to look nice and complete.
514 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
515 // and the last 1/3rd is the outro physics (bouncing)
516 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
517 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
518 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
519 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
520 towerFloatHeight = linearBlendHeight + parabola;
521 }
522
523 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
524
525 rlPushMatrix();
526 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
527
528 rlPushMatrix();
529 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
530 // calculate x and z rotation to align the model with the spring
531 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
532 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
533 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
534 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
535 float springLength = Vector3Length(towerUp);
536 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
537 float towerSquash = 1.0f / towerStretch;
538 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
539 rlScalef(towerSquash, towerStretch, towerSquash);
540 Tower dummy = {
541 .towerType = level->placementMode,
542 };
543 TowerDrawSingle(dummy);
544 rlPopMatrix();
545
546 // draw a shadow for the tower
547 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
548 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
549 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
550
551
552 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
553 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
554 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
555 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
556
557 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
558 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
559 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
560 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
561 rlPopMatrix();
562
563 guiState.isBlocked = 0;
564
565 EndMode3D();
566
567 TowerDrawHealthBars(level->camera);
568
569 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
570 {
571 if (level->placementTimer > placementDuration)
572 {
573 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
574 // testing repairing
575 tower->damage = 2.5f;
576 level->playerGold -= GetTowerCosts(level->placementMode);
577 level->nextState = LEVEL_STATE_BUILDING;
578 level->placementMode = TOWER_TYPE_NONE;
579 }
580 }
581 else
582 {
583 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
584 {
585 level->nextState = LEVEL_STATE_BUILDING;
586 level->placementMode = TOWER_TYPE_NONE;
587 TraceLog(LOG_INFO, "Cancel building");
588 }
589
590 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
591 {
592 level->placementPhase = PLACEMENT_PHASE_PLACING;
593 level->placementTimer = 0.0f;
594 }
595 }
596 }
597
598 void DrawLevelBuildingState(Level *level)
599 {
600 BeginMode3D(level->camera);
601 DrawLevelGround(level);
602
603 PathFindingMapUpdate(0, 0);
604 TowerDraw();
605 EnemyDraw();
606 ProjectileDraw();
607 ParticleDraw();
608 DrawEnemyPaths(level);
609
610 guiState.isBlocked = 0;
611
612 // when the context menu is not active, we update the placement position
613 if (level->placementContextMenuStatus == 0)
614 {
615 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
616 float hitDistance = ray.position.y / -ray.direction.y;
617 float hitX = ray.direction.x * hitDistance + ray.position.x;
618 float hitY = ray.direction.z * hitDistance + ray.position.z;
619 level->placementX = (int)floorf(hitX + 0.5f);
620 level->placementY = (int)floorf(hitY + 0.5f);
621 }
622
623 // Hover rectangle, when the mouse is over the map
624 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
625 if (isHovering)
626 {
627 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
628 }
629
630 EndMode3D();
631
632 TowerDrawHealthBars(level->camera);
633
634 // Draw the context menu when the context menu is active
635 if (level->placementContextMenuStatus >= 1)
636 {
637 const int itemHeight = 30;
638 const int itemSpacing = 5;
639 Tower *tower = TowerGetAt(level->placementX, level->placementY);
640 int itemCount = 4;
641 if (tower)
642 {
643 itemCount = 2;
644 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
645 {
646 // repair
647 itemCount++;
648 }
649
650 if (tower->towerType != TOWER_TYPE_BASE)
651 {
652 // sell
653 itemCount++;
654 }
655 }
656
657 // The context menu can open above or below the placement position
658 // Find both points and find out which position we want to use
659 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
660 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
661 Rectangle contextMenu = {0, 0, 150, (itemHeight + itemSpacing) * itemCount + itemSpacing};
662
663 if (anchorHigh.y > contextMenu.height) {
664 // context menu is above the placement position, which should be default
665 contextMenu.y = anchorHigh.y - contextMenu.height;
666 }
667 else {
668 // context menu is below the placement position
669 contextMenu.y = anchorLow.y;
670 }
671 // center the context menu, respecting the limits
672 contextMenu.x = anchorLow.x - contextMenu.width * 0.5f;
673 if (contextMenu.x < 0) {
674 contextMenu.x = 0;
675 }
676 if (contextMenu.x + contextMenu.width > GetScreenWidth()) {
677 contextMenu.x = GetScreenWidth() - contextMenu.width;
678 }
679
680 // handle closing the context menu
681 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
682 {
683 level->placementContextMenuStatus -= 1;
684 }
685
686 DrawRectangle(contextMenu.x, contextMenu.y, contextMenu.width, contextMenu.height, (Color){0, 0, 0, 128});
687 const int itemX = contextMenu.x + itemSpacing;
688 const int itemWidth = contextMenu.width - itemSpacing * 2;
689 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx)
690 #define ITEM_RECT(idx, marginLR) itemX + (marginLR), ITEM_Y(idx), itemWidth - (marginLR) * 2, itemHeight
691 if (tower == 0)
692 {
693 if (
694 DrawBuildingBuildButton(level, ITEM_RECT(0, 0), TOWER_TYPE_WALL, "Wall") ||
695 DrawBuildingBuildButton(level, ITEM_RECT(1, 0), TOWER_TYPE_ARCHER, "Archer") ||
696 DrawBuildingBuildButton(level, ITEM_RECT(2, 0), TOWER_TYPE_BALLISTA, "Ballista") ||
697 DrawBuildingBuildButton(level, ITEM_RECT(3, 0), TOWER_TYPE_CATAPULT, "Catapult"))
698 {
699 level->placementContextMenuStatus = 0;
700 }
701 }
702 else
703 {
704 switch (tower->towerType)
705 {
706 case TOWER_TYPE_BASE:
707 DrawBoxedText("Base", ITEM_RECT(0, itemSpacing), 0.5f, 0.5f, WHITE);
708 break;
709 case TOWER_TYPE_ARCHER:
710 DrawBoxedText("Archer", ITEM_RECT(0, itemSpacing), 0.5f, 0.5f, WHITE);
711 break;
712 case TOWER_TYPE_BALLISTA:
713 DrawBoxedText("Ballista", ITEM_RECT(0, itemSpacing), 0.5f, 0.5f, WHITE);
714 break;
715 case TOWER_TYPE_CATAPULT:
716 DrawBoxedText("Catapult", ITEM_RECT(0, itemSpacing), 0.5f, 0.5f, WHITE);
717 break;
718 case TOWER_TYPE_WALL:
719 DrawBoxedText("Wall", ITEM_RECT(0, itemSpacing), 0.5f, 0.5f, WHITE);
720 break;
721 }
722 float maxHitpoints = TowerGetMaxHealth(tower);
723 float hp = maxHitpoints - tower->damage;
724 DrawBoxedText("HP:", ITEM_RECT(1, itemSpacing), 0.0f, 0.5f, WHITE);
725 DrawBoxedText(TextFormat("%.1f / %.1f", hp, maxHitpoints), ITEM_RECT(1, itemSpacing), 1.0f, 0.5f, WHITE);
726
727 int menuPos = 2;
728 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
729 {
730 if (Button("Repair 1 (1G)", ITEM_RECT(menuPos, 0), 0))
731 {
732 level->playerGold -= 1;
733 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
734 // let menu stay open, next mouse release will decrease this to 1
735 level->placementContextMenuStatus = 2;
736 }
737 menuPos += 1;
738 }
739
740 if (tower->towerType != TOWER_TYPE_BASE)
741 {
742 float damageFactor = 1.0f - tower->damage / maxHitpoints;
743 int sellValue = (int) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
744 if (Button(TextFormat("Sell (%dG)", sellValue), ITEM_RECT(menuPos, 0), 0))
745 {
746 level->playerGold += (int) ceilf(GetTowerCosts(tower->towerType) * 0.5f);
747 tower->towerType = TOWER_TYPE_NONE;
748 // close and prevent reopening the context menu with next mouse release
749 level->placementContextMenuStatus = -1;
750 }
751 menuPos += 1;
752 }
753 }
754 }
755 // Activate the context menu when the mouse is clicked and the context menu is not active
756 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
757 {
758 level->placementContextMenuStatus += 1;
759 }
760
761 // undefine the macros so we don't cause trouble in other functions
762 #undef ITEM_Y
763 #undef ITEM_RECT
764
765
766 if (level->placementContextMenuStatus == 0)
767 {
768 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
769 {
770 level->nextState = LEVEL_STATE_RESET;
771 }
772
773 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
774 {
775 level->nextState = LEVEL_STATE_BATTLE;
776 }
777
778 const char *text = "Building phase";
779 int textWidth = MeasureText(text, 20);
780 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 10, 20, WHITE);
781 }
782
783 }
784
785 void InitBattleStateConditions(Level *level)
786 {
787 level->state = LEVEL_STATE_BATTLE;
788 level->nextState = LEVEL_STATE_NONE;
789 level->waveEndTimer = 0.0f;
790 for (int i = 0; i < 10; i++)
791 {
792 EnemyWave *wave = &level->waves[i];
793 wave->spawned = 0;
794 wave->timeToSpawnNext = wave->delay;
795 }
796 }
797
798 void DrawLevelBattleState(Level *level)
799 {
800 BeginMode3D(level->camera);
801 DrawLevelGround(level);
802 TowerDraw();
803 EnemyDraw();
804 ProjectileDraw();
805 ParticleDraw();
806 guiState.isBlocked = 0;
807 EndMode3D();
808
809 EnemyDrawHealthbars(level->camera);
810 TowerDrawHealthBars(level->camera);
811
812 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
813 {
814 level->nextState = LEVEL_STATE_RESET;
815 }
816
817 int maxCount = 0;
818 int remainingCount = 0;
819 for (int i = 0; i < 10; i++)
820 {
821 EnemyWave *wave = &level->waves[i];
822 if (wave->wave != level->currentWave)
823 {
824 continue;
825 }
826 maxCount += wave->count;
827 remainingCount += wave->count - wave->spawned;
828 }
829 int aliveCount = EnemyCount();
830 remainingCount += aliveCount;
831
832 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
833 int textWidth = MeasureText(text, 20);
834 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
835 }
836
837 void DrawLevel(Level *level)
838 {
839 switch (level->state)
840 {
841 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
842 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
843 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
844 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
845 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
846 default: break;
847 }
848
849 DrawLevelHud(level);
850 }
851
852 void UpdateLevel(Level *level)
853 {
854 if (level->state == LEVEL_STATE_BATTLE)
855 {
856 int activeWaves = 0;
857 for (int i = 0; i < 10; i++)
858 {
859 EnemyWave *wave = &level->waves[i];
860 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
861 {
862 continue;
863 }
864 activeWaves++;
865 wave->timeToSpawnNext -= gameTime.deltaTime;
866 if (wave->timeToSpawnNext <= 0.0f)
867 {
868 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
869 if (enemy)
870 {
871 wave->timeToSpawnNext = wave->interval;
872 wave->spawned++;
873 }
874 }
875 }
876 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
877 level->waveEndTimer += gameTime.deltaTime;
878 if (level->waveEndTimer >= 2.0f)
879 {
880 level->nextState = LEVEL_STATE_LOST_WAVE;
881 }
882 }
883 else if (activeWaves == 0 && EnemyCount() == 0)
884 {
885 level->waveEndTimer += gameTime.deltaTime;
886 if (level->waveEndTimer >= 2.0f)
887 {
888 level->nextState = LEVEL_STATE_WON_WAVE;
889 }
890 }
891 }
892
893 PathFindingMapUpdate(0, 0);
894 EnemyUpdate();
895 TowerUpdate();
896 ProjectileUpdate();
897 ParticleUpdate();
898
899 if (level->nextState == LEVEL_STATE_RESET)
900 {
901 InitLevel(level);
902 }
903
904 if (level->nextState == LEVEL_STATE_BATTLE)
905 {
906 InitBattleStateConditions(level);
907 }
908
909 if (level->nextState == LEVEL_STATE_WON_WAVE)
910 {
911 level->currentWave++;
912 level->state = LEVEL_STATE_WON_WAVE;
913 }
914
915 if (level->nextState == LEVEL_STATE_LOST_WAVE)
916 {
917 level->state = LEVEL_STATE_LOST_WAVE;
918 }
919
920 if (level->nextState == LEVEL_STATE_BUILDING)
921 {
922 level->state = LEVEL_STATE_BUILDING;
923 }
924
925 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
926 {
927 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
928 level->placementTransitionPosition = (Vector2){
929 level->placementX, level->placementY};
930 // initialize the spring to the current position
931 level->placementTowerSpring = (PhysicsPoint){
932 .position = (Vector3){level->placementX, 8.0f, level->placementY},
933 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
934 };
935 level->placementPhase = PLACEMENT_PHASE_STARTING;
936 level->placementTimer = 0.0f;
937 }
938
939 if (level->nextState == LEVEL_STATE_WON_LEVEL)
940 {
941 // make something of this later
942 InitLevel(level);
943 }
944
945 level->nextState = LEVEL_STATE_NONE;
946 }
947
948 float nextSpawnTime = 0.0f;
949
950 void ResetGame()
951 {
952 InitLevel(currentLevel);
953 }
954
955 void InitGame()
956 {
957 TowerInit();
958 EnemyInit();
959 ProjectileInit();
960 ParticleInit();
961 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
962
963 currentLevel = levels;
964 InitLevel(currentLevel);
965 }
966
967 //# Immediate GUI functions
968
969 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
970 {
971 const float healthBarHeight = 6.0f;
972 const float healthBarOffset = 15.0f;
973 const float inset = 2.0f;
974 const float innerWidth = healthBarWidth - inset * 2;
975 const float innerHeight = healthBarHeight - inset * 2;
976
977 Vector2 screenPos = GetWorldToScreen(position, camera);
978 screenPos = Vector2Add(screenPos, screenOffset);
979 float centerX = screenPos.x - healthBarWidth * 0.5f;
980 float topY = screenPos.y - healthBarOffset;
981 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
982 float healthWidth = innerWidth * healthRatio;
983 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
984 }
985
986 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
987 {
988 Font font = GetFontDefault();
989 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
990
991 DrawTextEx(font, text, (Vector2){
992 x + (width - textSize.x) * alignX,
993 y + (height - textSize.y) * alignY
994 }, font.baseSize * 2.0f, 1, textColor);
995 }
996
997 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
998 {
999 Rectangle bounds = {x, y, width, height};
1000 int isPressed = 0;
1001 int isSelected = state && state->isSelected;
1002 int isDisabled = state && state->isDisabled;
1003 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1004 {
1005 Color color = isSelected ? DARKGRAY : GRAY;
1006 DrawRectangle(x, y, width, height, color);
1007 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1008 {
1009 isPressed = 1;
1010 }
1011 guiState.isBlocked = 1;
1012 }
1013 else
1014 {
1015 Color color = isSelected ? WHITE : LIGHTGRAY;
1016 DrawRectangle(x, y, width, height, color);
1017 }
1018 Font font = GetFontDefault();
1019 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1020 Color textColor = isDisabled ? GRAY : BLACK;
1021 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
1022 return isPressed;
1023 }
1024
1025 //# Main game loop
1026
1027 void GameUpdate()
1028 {
1029 UpdateLevel(currentLevel);
1030 }
1031
1032 int main(void)
1033 {
1034 int screenWidth, screenHeight;
1035 GetPreferredSize(&screenWidth, &screenHeight);
1036 InitWindow(screenWidth, screenHeight, "Tower defense");
1037 float gamespeed = 1.0f;
1038 SetTargetFPS(30);
1039
1040 LoadAssets();
1041 InitGame();
1042
1043 float pause = 1.0f;
1044
1045 while (!WindowShouldClose())
1046 {
1047 if (IsPaused()) {
1048 // canvas is not visible in browser - do nothing
1049 continue;
1050 }
1051
1052 if (IsKeyPressed(KEY_T))
1053 {
1054 gamespeed += 0.1f;
1055 if (gamespeed > 1.05f) gamespeed = 0.1f;
1056 }
1057
1058 if (IsKeyPressed(KEY_P))
1059 {
1060 pause = pause > 0.5f ? 0.0f : 1.0f;
1061 }
1062
1063 float dt = GetFrameTime() * gamespeed * pause;
1064 // cap maximum delta time to 0.1 seconds to prevent large time steps
1065 if (dt > 0.1f) dt = 0.1f;
1066 gameTime.time += dt;
1067 gameTime.deltaTime = dt;
1068 gameTime.frameCount += 1;
1069
1070 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1071 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1072
1073 BeginDrawing();
1074 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1075
1076 GameUpdate();
1077 DrawLevel(currentLevel);
1078
1079 if (gamespeed != 1.0f)
1080 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1081 EndDrawing();
1082
1083 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1084 }
1085
1086 CloseWindow();
1087
1088 return 0;
1089 }
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
148 int16_t placementX;
149 int16_t placementY;
150 int8_t placementContextMenuStatus;
151
152 Vector2 placementTransitionPosition;
153 PhysicsPoint placementTowerSpring;
154
155 int initialGold;
156 int playerGold;
157
158 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
159 int currentWave;
160 float waveEndTimer;
161 } Level;
162
163 typedef struct DeltaSrc
164 {
165 char x, y;
166 } DeltaSrc;
167
168 typedef struct PathfindingMap
169 {
170 int width, height;
171 float scale;
172 float *distances;
173 long *towerIndex;
174 DeltaSrc *deltaSrc;
175 float maxDistance;
176 Matrix toMapSpace;
177 Matrix toWorldSpace;
178 } PathfindingMap;
179
180 // when we execute the pathfinding algorithm, we need to store the active nodes
181 // in a queue. Each node has a position, a distance from the start, and the
182 // position of the node that we came from.
183 typedef struct PathfindingNode
184 {
185 int16_t x, y, fromX, fromY;
186 float distance;
187 } PathfindingNode;
188
189 typedef struct EnemyId
190 {
191 uint16_t index;
192 uint16_t generation;
193 } EnemyId;
194
195 typedef struct EnemyClassConfig
196 {
197 float speed;
198 float health;
199 float shieldHealth;
200 float shieldDamageAbsorption;
201 float radius;
202 float maxAcceleration;
203 float requiredContactTime;
204 float explosionDamage;
205 float explosionRange;
206 float explosionPushbackPower;
207 int goldValue;
208 } EnemyClassConfig;
209
210 typedef struct Enemy
211 {
212 int16_t currentX, currentY;
213 int16_t nextX, nextY;
214 Vector2 simPosition;
215 Vector2 simVelocity;
216 uint16_t generation;
217 float walkedDistance;
218 float startMovingTime;
219 float damage, futureDamage;
220 float shieldDamage;
221 float contactTime;
222 uint8_t enemyType;
223 uint8_t movePathCount;
224 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
225 } Enemy;
226
227 // a unit that uses sprites to be drawn
228 #define SPRITE_UNIT_ANIMATION_COUNT 6
229 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
230 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
231 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
232
233 typedef struct SpriteAnimation
234 {
235 Rectangle srcRect;
236 Vector2 offset;
237 uint8_t animationId;
238 uint8_t frameCount;
239 uint8_t frameWidth;
240 float frameDuration;
241 } SpriteAnimation;
242
243 typedef struct SpriteUnit
244 {
245 float scale;
246 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
247 } SpriteUnit;
248
249 #define PROJECTILE_MAX_COUNT 1200
250 #define PROJECTILE_TYPE_NONE 0
251 #define PROJECTILE_TYPE_ARROW 1
252 #define PROJECTILE_TYPE_CATAPULT 2
253 #define PROJECTILE_TYPE_BALLISTA 3
254
255 typedef struct Projectile
256 {
257 uint8_t projectileType;
258 float shootTime;
259 float arrivalTime;
260 float distance;
261 Vector3 position;
262 Vector3 target;
263 Vector3 directionNormal;
264 EnemyId targetEnemy;
265 HitEffectConfig hitEffectConfig;
266 } Projectile;
267
268 //# Function declarations
269 float TowerGetMaxHealth(Tower *tower);
270 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
271 int EnemyAddDamageRange(Vector2 position, float range, float damage);
272 int EnemyAddDamage(Enemy *enemy, float damage);
273
274 //# Enemy functions
275 void EnemyInit();
276 void EnemyDraw();
277 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
278 void EnemyUpdate();
279 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
280 float EnemyGetMaxHealth(Enemy *enemy);
281 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
282 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
283 EnemyId EnemyGetId(Enemy *enemy);
284 Enemy *EnemyTryResolve(EnemyId enemyId);
285 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
286 int EnemyAddDamage(Enemy *enemy, float damage);
287 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
288 int EnemyCount();
289 void EnemyDrawHealthbars(Camera3D camera);
290
291 //# Tower functions
292 void TowerInit();
293 Tower *TowerGetAt(int16_t x, int16_t y);
294 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
295 Tower *GetTowerByType(uint8_t towerType);
296 int GetTowerCosts(uint8_t towerType);
297 float TowerGetMaxHealth(Tower *tower);
298 void TowerDraw();
299 void TowerDrawSingle(Tower tower);
300 void TowerUpdate();
301 void TowerDrawHealthBars(Camera3D camera);
302 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
303
304 //# Particles
305 void ParticleInit();
306 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
307 void ParticleUpdate();
308 void ParticleDraw();
309
310 //# Projectiles
311 void ProjectileInit();
312 void ProjectileDraw();
313 void ProjectileUpdate();
314 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
315
316 //# Pathfinding map
317 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
318 float PathFindingGetDistance(int mapX, int mapY);
319 Vector2 PathFindingGetGradient(Vector3 world);
320 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
321 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
322 void PathFindingMapDraw();
323
324 //# UI
325 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
326
327 //# Level
328 void DrawLevelGround(Level *level);
329 void DrawEnemyPath(Level *level, Color arrowColor);
330
331 //# variables
332 extern Level *currentLevel;
333 extern Enemy enemies[ENEMY_MAX_COUNT];
334 extern int enemyCount;
335 extern EnemyClassConfig enemyClassConfigs[];
336
337 extern GUIState guiState;
338 extern GameTime gameTime;
339 extern Tower towers[TOWER_MAX_COUNT];
340 extern int towerCount;
341
342 extern Texture2D palette, spriteSheet;
343
344 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 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 #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 #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
For the purpose of testing, the towers spawn here with some damage already. The context menu looks like this:

The code is a bit messy here due to the integration of drawing and event handling in one go. Abstracting this could help to make this more sane. But before doing that, it is not the worst idea to try out the most straightforward way to be able to judge what is needed and if it is worth it. I once made the experiment of comparing an abstraction I wrote with a simple implementation and it turned out that the simple implementation was shorter and easier to understand. It's a healthy exercise to do this from time to time. It can be quite sobering.
One thing you might find confusing is the usage of macros here to shorten some function arguments, so let's have a look at this:
1 const int itemX = contextMenu.x + itemSpacing;
2 const int itemWidth = contextMenu.width - itemSpacing * 2;
3 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx)
4 #define ITEM_RECT(idx, marginLR) itemX + (marginLR), ITEM_Y(idx), itemWidth - (marginLR) * 2, itemHeight
5 if (tower == 0)
6 {
7 if (
8 DrawBuildingBuildButton(level, ITEM_RECT(0, 0), TOWER_TYPE_WALL, "Wall") ||
9 DrawBuildingBuildButton(level, ITEM_RECT(1, 0), TOWER_TYPE_ARCHER, "Archer") ||
10 DrawBuildingBuildButton(level, ITEM_RECT(2, 0), TOWER_TYPE_BALLISTA, "Ballista") ||
11 DrawBuildingBuildButton(level, ITEM_RECT(3, 0), TOWER_TYPE_CATAPULT, "Catapult"))
12 {
13 level->placementContextMenuStatus = 0;
14 }
15 }
Macros are pure text replacements. There is no syntax checking or type checking in the macro body. Only the final result is checked by the compiler.
Using macros is considered to be a double-edged sword. It can make the code harder to debug and understand, but it can also make the code more readable and maintainable.
Also, let's compare it with the previous code we had for the context menu:
1 if (tower == 0)
2 {
3 if (
4 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 5, contextMenu.width - 10, 30, TOWER_TYPE_WALL, "Wall") ||
5 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 40, contextMenu.width - 10, 30, TOWER_TYPE_ARCHER, "Archer") ||
6 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 75, contextMenu.width - 10, 30, TOWER_TYPE_BALLISTA, "Ballista") ||
7 DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 110, contextMenu.width - 10, 30, TOWER_TYPE_CATAPULT, "Catapult"))
8 {
9 level->placementContextMenuStatus = 0;
10 }
11 }
The first obvious difference is, that the macro version is shorter. The second difference is, that the original version used hand calculated values for the y position, while the macro version uses a formula. This makes it easier to adjust the spacing between the items when needed or inserting new items. The brevitity of the macro version also makes it easier to read.
What is a bit special is, that the ITEM_RECT inserts all 4 arguments into the function call. This is a bit unusual, but it is a useful trick to know.
Tower upgrading
Now that we have the context menu for towers, we can think about how to implement the upgrading. As the game designer noob I am, I will simply pick a few rules out of the blue:
- Range, damage and attack speed can be upgraded individually
- Any upgrade increases the level of the tower
- Each level increases the cost of the next upgrade
- A maximum level is reached at some point
Since the height of the context menu is limited to half the screen size and we have a lot of items to display, we need to organize the items in submenus. Some options would also benefit from a confirmation dialog, like when selling a tower. This is a point where reworking the code a little is necessary to avoid making this a total mess. So let's first come up with a structure and a function that can handle the drawing and event handling of the context menu without changing the behavior:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6 #include <string.h>
7
8 //# Variables
9 GUIState guiState = {0};
10 GameTime gameTime = {
11 .fixedDeltaTime = 1.0f / 60.0f,
12 };
13
14 Model floorTileAModel = {0};
15 Model floorTileBModel = {0};
16 Model treeModel[2] = {0};
17 Model firTreeModel[2] = {0};
18 Model rockModels[5] = {0};
19 Model grassPatchModel[1] = {0};
20
21 Model pathArrowModel = {0};
22 Model greenArrowModel = {0};
23
24 Texture2D palette, spriteSheet;
25
26 Level levels[] = {
27 [0] = {
28 .state = LEVEL_STATE_BUILDING,
29 .initialGold = 20,
30 .waves[0] = {
31 .enemyType = ENEMY_TYPE_SHIELD,
32 .wave = 0,
33 .count = 1,
34 .interval = 2.5f,
35 .delay = 1.0f,
36 .spawnPosition = {2, 6},
37 },
38 .waves[1] = {
39 .enemyType = ENEMY_TYPE_RUNNER,
40 .wave = 0,
41 .count = 5,
42 .interval = 0.5f,
43 .delay = 1.0f,
44 .spawnPosition = {-2, 6},
45 },
46 .waves[2] = {
47 .enemyType = ENEMY_TYPE_SHIELD,
48 .wave = 1,
49 .count = 20,
50 .interval = 1.5f,
51 .delay = 1.0f,
52 .spawnPosition = {0, 6},
53 },
54 .waves[3] = {
55 .enemyType = ENEMY_TYPE_MINION,
56 .wave = 2,
57 .count = 30,
58 .interval = 1.2f,
59 .delay = 1.0f,
60 .spawnPosition = {2, 6},
61 },
62 .waves[4] = {
63 .enemyType = ENEMY_TYPE_BOSS,
64 .wave = 2,
65 .count = 2,
66 .interval = 5.0f,
67 .delay = 2.0f,
68 .spawnPosition = {-2, 4},
69 }
70 },
71 };
72
73 Level *currentLevel = levels;
74
75 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
76
77 //# Game
78
79 static Model LoadGLBModel(char *filename)
80 {
81 Model model = LoadModel(TextFormat("data/%s.glb",filename));
82 for (int i = 0; i < model.materialCount; i++)
83 {
84 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
85 }
86 return model;
87 }
88
89 void LoadAssets()
90 {
91 // load a sprite sheet that contains all units
92 spriteSheet = LoadTexture("data/spritesheet.png");
93 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
94
95 // we'll use a palette texture to colorize the all buildings and environment art
96 palette = LoadTexture("data/palette.png");
97 // The texture uses gradients on very small space, so we'll enable bilinear filtering
98 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
99
100 floorTileAModel = LoadGLBModel("floor-tile-a");
101 floorTileBModel = LoadGLBModel("floor-tile-b");
102 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
103 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
104 firTreeModel[0] = LoadGLBModel("firtree-1-a");
105 firTreeModel[1] = LoadGLBModel("firtree-1-b");
106 rockModels[0] = LoadGLBModel("rock-1");
107 rockModels[1] = LoadGLBModel("rock-2");
108 rockModels[2] = LoadGLBModel("rock-3");
109 rockModels[3] = LoadGLBModel("rock-4");
110 rockModels[4] = LoadGLBModel("rock-5");
111 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
112
113 pathArrowModel = LoadGLBModel("direction-arrow-x");
114 greenArrowModel = LoadGLBModel("green-arrow");
115 }
116
117 void InitLevel(Level *level)
118 {
119 level->seed = (int)(GetTime() * 100.0f);
120
121 TowerInit();
122 EnemyInit();
123 ProjectileInit();
124 ParticleInit();
125 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
126
127 level->placementMode = 0;
128 level->state = LEVEL_STATE_BUILDING;
129 level->nextState = LEVEL_STATE_NONE;
130 level->playerGold = level->initialGold;
131 level->currentWave = 0;
132 level->placementX = -1;
133 level->placementY = 0;
134
135 Camera *camera = &level->camera;
136 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
137 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
138 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
139 camera->fovy = 11.5f;
140 camera->projection = CAMERA_ORTHOGRAPHIC;
141 }
142
143 void DrawLevelHud(Level *level)
144 {
145 const char *text = TextFormat("Gold: %d", level->playerGold);
146 Font font = GetFontDefault();
147 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
148 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
149 }
150
151 void DrawLevelReportLostWave(Level *level)
152 {
153 BeginMode3D(level->camera);
154 DrawLevelGround(level);
155 TowerDraw();
156 EnemyDraw();
157 ProjectileDraw();
158 ParticleDraw();
159 guiState.isBlocked = 0;
160 EndMode3D();
161
162 TowerDrawHealthBars(level->camera);
163
164 const char *text = "Wave lost";
165 int textWidth = MeasureText(text, 20);
166 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
167
168 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
169 {
170 level->nextState = LEVEL_STATE_RESET;
171 }
172 }
173
174 int HasLevelNextWave(Level *level)
175 {
176 for (int i = 0; i < 10; i++)
177 {
178 EnemyWave *wave = &level->waves[i];
179 if (wave->wave == level->currentWave)
180 {
181 return 1;
182 }
183 }
184 return 0;
185 }
186
187 void DrawLevelReportWonWave(Level *level)
188 {
189 BeginMode3D(level->camera);
190 DrawLevelGround(level);
191 TowerDraw();
192 EnemyDraw();
193 ProjectileDraw();
194 ParticleDraw();
195 guiState.isBlocked = 0;
196 EndMode3D();
197
198 TowerDrawHealthBars(level->camera);
199
200 const char *text = "Wave won";
201 int textWidth = MeasureText(text, 20);
202 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
203
204
205 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
206 {
207 level->nextState = LEVEL_STATE_RESET;
208 }
209
210 if (HasLevelNextWave(level))
211 {
212 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
213 {
214 level->nextState = LEVEL_STATE_BUILDING;
215 }
216 }
217 else {
218 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
219 {
220 level->nextState = LEVEL_STATE_WON_LEVEL;
221 }
222 }
223 }
224
225 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
226 {
227 static ButtonState buttonStates[8] = {0};
228 int cost = GetTowerCosts(towerType);
229 const char *text = TextFormat("%s: %d", name, cost);
230 buttonStates[towerType].isSelected = level->placementMode == towerType;
231 buttonStates[towerType].isDisabled = level->playerGold < cost;
232 if (Button(text, x, y, width, height, &buttonStates[towerType]))
233 {
234 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
235 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
236 return 1;
237 }
238 return 0;
239 }
240
241 float GetRandomFloat(float min, float max)
242 {
243 int random = GetRandomValue(0, 0xfffffff);
244 return ((float)random / (float)0xfffffff) * (max - min) + min;
245 }
246
247 void DrawLevelGround(Level *level)
248 {
249 // draw checkerboard ground pattern
250 for (int x = -5; x <= 5; x += 1)
251 {
252 for (int y = -5; y <= 5; y += 1)
253 {
254 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
255 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
256 }
257 }
258
259 int oldSeed = GetRandomValue(0, 0xfffffff);
260 SetRandomSeed(level->seed);
261 // increase probability for trees via duplicated entries
262 Model borderModels[64];
263 int maxRockCount = GetRandomValue(2, 6);
264 int maxTreeCount = GetRandomValue(10, 20);
265 int maxFirTreeCount = GetRandomValue(5, 10);
266 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
267 int grassPatchCount = GetRandomValue(5, 30);
268
269 int modelCount = 0;
270 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
271 {
272 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
273 }
274 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
275 {
276 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
277 }
278 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
279 {
280 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
281 }
282 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
283 {
284 borderModels[modelCount++] = grassPatchModel[0];
285 }
286
287 // draw some objects around the border of the map
288 Vector3 up = {0, 1, 0};
289 // a pseudo random number generator to get the same result every time
290 const float wiggle = 0.75f;
291 const int layerCount = 3;
292 for (int layer = 0; layer <= layerCount; layer++)
293 {
294 int layerPos = 6 + layer;
295 Model *selectedModels = borderModels;
296 int selectedModelCount = modelCount;
297 if (layer == 0)
298 {
299 selectedModels = grassPatchModel;
300 selectedModelCount = 1;
301 }
302 for (int x = -6 - layer; x <= 6 + layer; x += 1)
303 {
304 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
305 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
306 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
307 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
308 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
309 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
310 }
311
312 for (int z = -5 - layer; z <= 5 + layer; z += 1)
313 {
314 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
315 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
316 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
317 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
318 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
319 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
320 }
321 }
322
323 SetRandomSeed(oldSeed);
324 }
325
326 void DrawEnemyPath(Level *level, Color arrowColor)
327 {
328 const int castleX = 0, castleY = 0;
329 const int maxWaypointCount = 200;
330 const float timeStep = 1.0f;
331 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
332
333 // we start with a time offset to simulate the path,
334 // this way the arrows are animated in a forward moving direction
335 // The time is wrapped around the time step to get a smooth animation
336 float timeOffset = fmodf(GetTime(), timeStep);
337
338 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
339 {
340 EnemyWave *wave = &level->waves[i];
341 if (wave->wave != level->currentWave)
342 {
343 continue;
344 }
345
346 // use this dummy enemy to simulate the path
347 Enemy dummy = {
348 .enemyType = ENEMY_TYPE_MINION,
349 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
350 .nextX = wave->spawnPosition.x,
351 .nextY = wave->spawnPosition.y,
352 .currentX = wave->spawnPosition.x,
353 .currentY = wave->spawnPosition.y,
354 };
355
356 float deltaTime = timeOffset;
357 for (int j = 0; j < maxWaypointCount; j++)
358 {
359 int waypointPassedCount = 0;
360 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
361 // after the initial variable starting offset, we use a fixed time step
362 deltaTime = timeStep;
363 dummy.simPosition = pos;
364
365 // Update the dummy's position just like we do in the regular enemy update loop
366 for (int k = 0; k < waypointPassedCount; k++)
367 {
368 dummy.currentX = dummy.nextX;
369 dummy.currentY = dummy.nextY;
370 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
371 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
372 {
373 break;
374 }
375 }
376 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
377 {
378 break;
379 }
380
381 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
382 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
383 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
384 }
385 }
386 }
387
388 void DrawEnemyPaths(Level *level)
389 {
390 // disable depth testing for the path arrows
391 // flush the 3D batch to draw the arrows on top of everything
392 rlDrawRenderBatchActive();
393 rlDisableDepthTest();
394 DrawEnemyPath(level, (Color){64, 64, 64, 160});
395
396 rlDrawRenderBatchActive();
397 rlEnableDepthTest();
398 DrawEnemyPath(level, WHITE);
399 }
400
401 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
402 {
403 float dt = gameTime.fixedDeltaTime;
404 // smooth transition for the placement position using exponential decay
405 const float lambda = 15.0f;
406 float factor = 1.0f - expf(-lambda * dt);
407
408 float damping = 0.5f;
409 float springStiffness = 300.0f;
410 float springDecay = 95.0f;
411 float minHeight = 0.35f;
412
413 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
414 {
415 damping = 1.0f;
416 springDecay = 90.0f;
417 springStiffness = 100.0f;
418 minHeight = 0.70f;
419 }
420
421 for (int i = 0; i < gameTime.fixedStepCount; i++)
422 {
423 level->placementTransitionPosition =
424 Vector2Lerp(
425 level->placementTransitionPosition,
426 (Vector2){mapX, mapY}, factor);
427
428 // draw the spring position for debugging the spring simulation
429 // first step: stiff spring, no simulation
430 Vector3 worldPlacementPosition = (Vector3){
431 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
432 Vector3 springTargetPosition = (Vector3){
433 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
434 // consider the current velocity to predict the future position in order to dampen
435 // the spring simulation. Longer prediction times will result in more damping
436 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
437 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
438 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
439 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
440 // decay velocity of the upright forcing spring
441 // This force acts like a 2nd spring that pulls the tip upright into the air above the
442 // base position
443 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
444 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
445
446 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
447 // we use a simple spring model with a rest length of 1.0f
448 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
449 float springLength = Vector3Length(springDelta);
450 float springForce = (springLength - 1.0f) * springStiffness;
451 Vector3 springForceVector = Vector3Normalize(springDelta);
452 springForceVector = Vector3Scale(springForceVector, springForce);
453 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
454 Vector3Scale(springForceVector, dt));
455
456 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
457 Vector3Scale(level->placementTowerSpring.velocity, dt));
458 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
459 {
460 level->placementTowerSpring.velocity.y *= -1.0f;
461 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
462 }
463 }
464 }
465
466 void DrawLevelBuildingPlacementState(Level *level)
467 {
468 const float placementDuration = 0.5f;
469
470 level->placementTimer += gameTime.deltaTime;
471 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
472 {
473 level->placementPhase = PLACEMENT_PHASE_MOVING;
474 level->placementTimer = 0.0f;
475 }
476
477 BeginMode3D(level->camera);
478 DrawLevelGround(level);
479
480 int blockedCellCount = 0;
481 Vector2 blockedCells[1];
482 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
483 float planeDistance = ray.position.y / -ray.direction.y;
484 float planeX = ray.direction.x * planeDistance + ray.position.x;
485 float planeY = ray.direction.z * planeDistance + ray.position.z;
486 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
487 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
488 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
489 level->placementMode && !guiState.isBlocked &&
490 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
491 {
492 level->placementX = mapX;
493 level->placementY = mapY;
494 }
495 else
496 {
497 mapX = level->placementX;
498 mapY = level->placementY;
499 }
500 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
501 PathFindingMapUpdate(blockedCellCount, blockedCells);
502
503 TowerDraw();
504 EnemyDraw();
505 ProjectileDraw();
506 ParticleDraw();
507 DrawEnemyPaths(level);
508
509 // let the tower float up and down. Consider this height in the spring simulation as well
510 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
511
512 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
513 {
514 // The bouncing spring needs a bit of outro time to look nice and complete.
515 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
516 // and the last 1/3rd is the outro physics (bouncing)
517 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
518 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
519 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
520 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
521 towerFloatHeight = linearBlendHeight + parabola;
522 }
523
524 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
525
526 rlPushMatrix();
527 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
528
529 rlPushMatrix();
530 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
531 // calculate x and z rotation to align the model with the spring
532 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
533 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
534 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
535 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
536 float springLength = Vector3Length(towerUp);
537 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
538 float towerSquash = 1.0f / towerStretch;
539 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
540 rlScalef(towerSquash, towerStretch, towerSquash);
541 Tower dummy = {
542 .towerType = level->placementMode,
543 };
544 TowerDrawSingle(dummy);
545 rlPopMatrix();
546
547 // draw a shadow for the tower
548 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
549 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
550 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
551
552
553 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
554 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
555 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
556 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
557
558 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
559 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
560 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
561 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
562 rlPopMatrix();
563
564 guiState.isBlocked = 0;
565
566 EndMode3D();
567
568 TowerDrawHealthBars(level->camera);
569
570 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
571 {
572 if (level->placementTimer > placementDuration)
573 {
574 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
575 // testing repairing
576 tower->damage = 2.5f;
577 level->playerGold -= GetTowerCosts(level->placementMode);
578 level->nextState = LEVEL_STATE_BUILDING;
579 level->placementMode = TOWER_TYPE_NONE;
580 }
581 }
582 else
583 {
584 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
585 {
586 level->nextState = LEVEL_STATE_BUILDING;
587 level->placementMode = TOWER_TYPE_NONE;
588 TraceLog(LOG_INFO, "Cancel building");
589 }
590
591 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
592 {
593 level->placementPhase = PLACEMENT_PHASE_PLACING;
594 level->placementTimer = 0.0f;
595 }
596 }
597 }
598
599 typedef struct ContextMenuArgs
600 {
601 void *data;
602 uint8_t uint8;
603 int32_t int32;
604 Tower *tower;
605 } ContextMenuArgs;
606
607 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
608 {
609 uint8_t towerType = data->uint8;
610 level->placementMode = towerType;
611 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
612 return 1;
613 }
614
615 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
616 {
617 Tower *tower = data->tower;
618 int gold = data->int32;
619 level->playerGold += gold;
620 tower->towerType = TOWER_TYPE_NONE;
621 return 1;
622 }
623
624 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
625 {
626 Tower *tower = data->tower;
627 if (level->playerGold >= 1)
628 {
629 level->playerGold -= 1;
630 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
631 }
632 return tower->damage == 0.0f;
633 }
634
635 typedef struct ContextMenuItem
636 {
637 uint8_t index;
638 char text[24];
639 float alignX;
640 int (*action)(Level*, ContextMenuArgs*);
641 void *data;
642 ContextMenuArgs args;
643 ButtonState buttonState;
644 } ContextMenuItem;
645
646 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
647 {
648 ContextMenuItem item = {.index = index, .alignX = alignX};
649 strncpy(item.text, text, 24);
650 return item;
651 }
652
653 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
654 {
655 ContextMenuItem item = {.index = index, .action = action, .args = args};
656 strncpy(item.text, text, 24);
657 return item;
658 }
659
660 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
661 {
662 const int itemHeight = 28;
663 const int itemSpacing = 4;
664 int itemCount = 0;
665 for (int i = 0; menus[i].text[0]; i++)
666 {
667 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
668 }
669
670 Rectangle contextMenu = {0, 0, width,
671 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing};
672
673 Vector2 anchor = anchorHigh.y > contextMenu.height ? anchorHigh : anchorLow;
674 float anchorPivotY = anchorHigh.y > contextMenu.height ? 1.0f : 0.0f;
675
676 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
677 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
678 DrawRectangle(contextMenu.x, contextMenu.y, contextMenu.width, contextMenu.height, (Color){0, 0, 0, 128});
679 const int itemX = contextMenu.x + itemSpacing;
680 const int itemWidth = contextMenu.width - itemSpacing * 2;
681 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx)
682 #define ITEM_RECT(idx, marginLR) itemX + (marginLR), ITEM_Y(idx), itemWidth - (marginLR) * 2, itemHeight
683 int status = 0;
684 for (int i = 0; menus[i].text[0]; i++)
685 {
686 if (menus[i].action)
687 {
688 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
689 {
690 status = menus[i].action(level, &menus[i].args);
691 }
692 }
693 else
694 {
695 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
696 }
697 }
698
699 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
700 {
701 return 1;
702 }
703
704 return status;
705 }
706
707 void DrawLevelBuildingState(Level *level)
708 {
709 BeginMode3D(level->camera);
710 DrawLevelGround(level);
711
712 PathFindingMapUpdate(0, 0);
713 TowerDraw();
714 EnemyDraw();
715 ProjectileDraw();
716 ParticleDraw();
717 DrawEnemyPaths(level);
718
719 guiState.isBlocked = 0;
720
721 // when the context menu is not active, we update the placement position
722 if (level->placementContextMenuStatus == 0)
723 {
724 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
725 float hitDistance = ray.position.y / -ray.direction.y;
726 float hitX = ray.direction.x * hitDistance + ray.position.x;
727 float hitY = ray.direction.z * hitDistance + ray.position.z;
728 level->placementX = (int)floorf(hitX + 0.5f);
729 level->placementY = (int)floorf(hitY + 0.5f);
730 }
731
732 // Hover rectangle, when the mouse is over the map
733 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
734 if (isHovering)
735 {
736 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
737 }
738
739 EndMode3D();
740
741 TowerDrawHealthBars(level->camera);
742
743 // Draw the context menu when the context menu is active
744 if (level->placementContextMenuStatus >= 1)
745 {
746 Tower *tower = TowerGetAt(level->placementX, level->placementY);
747 ContextMenuItem menu[12] = {0};
748 int menuCount = 0;
749 int menuIndex = 0;
750 if (tower)
751 {
752 float maxHitpoints = TowerGetMaxHealth(tower);
753 float hp = maxHitpoints - tower->damage;
754
755 if (tower) {
756 const char *towerNames[TOWER_TYPE_COUNT] = {
757 [TOWER_TYPE_BASE] = "Castle",
758 [TOWER_TYPE_WALL] = "Wall",
759 [TOWER_TYPE_BALLISTA] = "Ballista",
760 [TOWER_TYPE_ARCHER] = "Archer",
761 [TOWER_TYPE_CATAPULT] = "Catapult",
762 };
763
764 menu[menuCount++] = ContextMenuItemText(menuIndex++, towerNames[tower->towerType], 0.5f);
765 }
766
767 // two texts, same line
768 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
769 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
770
771 if (tower->towerType != TOWER_TYPE_BASE)
772 {
773 float damageFactor = 1.0f - tower->damage / maxHitpoints;
774 int32_t sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
775
776 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
777 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
778 }
779 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
780 {
781 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
782 (ContextMenuArgs){.tower = tower});
783 }
784 }
785 else
786 {
787 menu[menuCount++] = ContextMenuItemButton(menuIndex++,
788 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)),
789 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
790 menu[menuCount++] = ContextMenuItemButton(menuIndex++,
791 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)),
792 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
793 menu[menuCount++] = ContextMenuItemButton(menuIndex++,
794 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)),
795 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
796 menu[menuCount++] = ContextMenuItemButton(menuIndex++,
797 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)),
798 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
799 }
800
801 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
802 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
803 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
804 {
805 level->placementContextMenuStatus = -1;
806 }
807 }
808
809 // Activate the context menu when the mouse is clicked and the context menu is not active
810 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
811 {
812 level->placementContextMenuStatus += 1;
813 }
814
815 // undefine the macros so we don't cause trouble in other functions
816 #undef ITEM_Y
817 #undef ITEM_RECT
818
819
820 if (level->placementContextMenuStatus == 0)
821 {
822 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
823 {
824 level->nextState = LEVEL_STATE_RESET;
825 }
826
827 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
828 {
829 level->nextState = LEVEL_STATE_BATTLE;
830 }
831
832 const char *text = "Building phase";
833 int textWidth = MeasureText(text, 20);
834 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 10, 20, WHITE);
835 }
836
837 }
838
839 void InitBattleStateConditions(Level *level)
840 {
841 level->state = LEVEL_STATE_BATTLE;
842 level->nextState = LEVEL_STATE_NONE;
843 level->waveEndTimer = 0.0f;
844 for (int i = 0; i < 10; i++)
845 {
846 EnemyWave *wave = &level->waves[i];
847 wave->spawned = 0;
848 wave->timeToSpawnNext = wave->delay;
849 }
850 }
851
852 void DrawLevelBattleState(Level *level)
853 {
854 BeginMode3D(level->camera);
855 DrawLevelGround(level);
856 TowerDraw();
857 EnemyDraw();
858 ProjectileDraw();
859 ParticleDraw();
860 guiState.isBlocked = 0;
861 EndMode3D();
862
863 EnemyDrawHealthbars(level->camera);
864 TowerDrawHealthBars(level->camera);
865
866 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
867 {
868 level->nextState = LEVEL_STATE_RESET;
869 }
870
871 int maxCount = 0;
872 int remainingCount = 0;
873 for (int i = 0; i < 10; i++)
874 {
875 EnemyWave *wave = &level->waves[i];
876 if (wave->wave != level->currentWave)
877 {
878 continue;
879 }
880 maxCount += wave->count;
881 remainingCount += wave->count - wave->spawned;
882 }
883 int aliveCount = EnemyCount();
884 remainingCount += aliveCount;
885
886 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
887 int textWidth = MeasureText(text, 20);
888 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
889 }
890
891 void DrawLevel(Level *level)
892 {
893 switch (level->state)
894 {
895 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
896 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
897 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
898 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
899 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
900 default: break;
901 }
902
903 DrawLevelHud(level);
904 }
905
906 void UpdateLevel(Level *level)
907 {
908 if (level->state == LEVEL_STATE_BATTLE)
909 {
910 int activeWaves = 0;
911 for (int i = 0; i < 10; i++)
912 {
913 EnemyWave *wave = &level->waves[i];
914 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
915 {
916 continue;
917 }
918 activeWaves++;
919 wave->timeToSpawnNext -= gameTime.deltaTime;
920 if (wave->timeToSpawnNext <= 0.0f)
921 {
922 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
923 if (enemy)
924 {
925 wave->timeToSpawnNext = wave->interval;
926 wave->spawned++;
927 }
928 }
929 }
930 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
931 level->waveEndTimer += gameTime.deltaTime;
932 if (level->waveEndTimer >= 2.0f)
933 {
934 level->nextState = LEVEL_STATE_LOST_WAVE;
935 }
936 }
937 else if (activeWaves == 0 && EnemyCount() == 0)
938 {
939 level->waveEndTimer += gameTime.deltaTime;
940 if (level->waveEndTimer >= 2.0f)
941 {
942 level->nextState = LEVEL_STATE_WON_WAVE;
943 }
944 }
945 }
946
947 PathFindingMapUpdate(0, 0);
948 EnemyUpdate();
949 TowerUpdate();
950 ProjectileUpdate();
951 ParticleUpdate();
952
953 if (level->nextState == LEVEL_STATE_RESET)
954 {
955 InitLevel(level);
956 }
957
958 if (level->nextState == LEVEL_STATE_BATTLE)
959 {
960 InitBattleStateConditions(level);
961 }
962
963 if (level->nextState == LEVEL_STATE_WON_WAVE)
964 {
965 level->currentWave++;
966 level->state = LEVEL_STATE_WON_WAVE;
967 }
968
969 if (level->nextState == LEVEL_STATE_LOST_WAVE)
970 {
971 level->state = LEVEL_STATE_LOST_WAVE;
972 }
973
974 if (level->nextState == LEVEL_STATE_BUILDING)
975 {
976 level->state = LEVEL_STATE_BUILDING;
977 level->placementContextMenuStatus = 0;
978 }
979
980 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
981 {
982 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
983 level->placementTransitionPosition = (Vector2){
984 level->placementX, level->placementY};
985 // initialize the spring to the current position
986 level->placementTowerSpring = (PhysicsPoint){
987 .position = (Vector3){level->placementX, 8.0f, level->placementY},
988 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
989 };
990 level->placementPhase = PLACEMENT_PHASE_STARTING;
991 level->placementTimer = 0.0f;
992 }
993
994 if (level->nextState == LEVEL_STATE_WON_LEVEL)
995 {
996 // make something of this later
997 InitLevel(level);
998 }
999
1000 level->nextState = LEVEL_STATE_NONE;
1001 }
1002
1003 float nextSpawnTime = 0.0f;
1004
1005 void ResetGame()
1006 {
1007 InitLevel(currentLevel);
1008 }
1009
1010 void InitGame()
1011 {
1012 TowerInit();
1013 EnemyInit();
1014 ProjectileInit();
1015 ParticleInit();
1016 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1017
1018 currentLevel = levels;
1019 InitLevel(currentLevel);
1020 }
1021
1022 //# Immediate GUI functions
1023
1024 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1025 {
1026 const float healthBarHeight = 6.0f;
1027 const float healthBarOffset = 15.0f;
1028 const float inset = 2.0f;
1029 const float innerWidth = healthBarWidth - inset * 2;
1030 const float innerHeight = healthBarHeight - inset * 2;
1031
1032 Vector2 screenPos = GetWorldToScreen(position, camera);
1033 screenPos = Vector2Add(screenPos, screenOffset);
1034 float centerX = screenPos.x - healthBarWidth * 0.5f;
1035 float topY = screenPos.y - healthBarOffset;
1036 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1037 float healthWidth = innerWidth * healthRatio;
1038 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1039 }
1040
1041 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1042 {
1043 Font font = GetFontDefault();
1044 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1045
1046 DrawTextEx(font, text, (Vector2){
1047 x + (width - textSize.x) * alignX,
1048 y + (height - textSize.y) * alignY
1049 }, font.baseSize * 2.0f, 1, textColor);
1050 }
1051
1052 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1053 {
1054 Rectangle bounds = {x, y, width, height};
1055 int isPressed = 0;
1056 int isSelected = state && state->isSelected;
1057 int isDisabled = state && state->isDisabled;
1058 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1059 {
1060 Color color = isSelected ? DARKGRAY : GRAY;
1061 DrawRectangle(x, y, width, height, color);
1062 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1063 {
1064 isPressed = 1;
1065 }
1066 guiState.isBlocked = 1;
1067 }
1068 else
1069 {
1070 Color color = isSelected ? WHITE : LIGHTGRAY;
1071 DrawRectangle(x, y, width, height, color);
1072 }
1073 Font font = GetFontDefault();
1074 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1075 Color textColor = isDisabled ? GRAY : BLACK;
1076 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
1077 return isPressed;
1078 }
1079
1080 //# Main game loop
1081
1082 void GameUpdate()
1083 {
1084 UpdateLevel(currentLevel);
1085 }
1086
1087 int main(void)
1088 {
1089 int screenWidth, screenHeight;
1090 GetPreferredSize(&screenWidth, &screenHeight);
1091 InitWindow(screenWidth, screenHeight, "Tower defense");
1092 float gamespeed = 1.0f;
1093 SetTargetFPS(30);
1094
1095 LoadAssets();
1096 InitGame();
1097
1098 float pause = 1.0f;
1099
1100 while (!WindowShouldClose())
1101 {
1102 if (IsPaused()) {
1103 // canvas is not visible in browser - do nothing
1104 continue;
1105 }
1106
1107 if (IsKeyPressed(KEY_T))
1108 {
1109 gamespeed += 0.1f;
1110 if (gamespeed > 1.05f) gamespeed = 0.1f;
1111 }
1112
1113 if (IsKeyPressed(KEY_P))
1114 {
1115 pause = pause > 0.5f ? 0.0f : 1.0f;
1116 }
1117
1118 float dt = GetFrameTime() * gamespeed * pause;
1119 // cap maximum delta time to 0.1 seconds to prevent large time steps
1120 if (dt > 0.1f) dt = 0.1f;
1121 gameTime.time += dt;
1122 gameTime.deltaTime = dt;
1123 gameTime.frameCount += 1;
1124
1125 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1126 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1127
1128 BeginDrawing();
1129 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1130
1131 GameUpdate();
1132 DrawLevel(currentLevel);
1133
1134 if (gamespeed != 1.0f)
1135 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1136 EndDrawing();
1137
1138 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1139 }
1140
1141 CloseWindow();
1142
1143 return 0;
1144 }
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 TowerUpgradeState
73 {
74 uint8_t range;
75 uint8_t damage;
76 uint8_t speed;
77 } TowerUpgradeState;
78
79 typedef struct Tower
80 {
81 int16_t x, y;
82 uint8_t towerType;
83 TowerUpgradeState upgradeState;
84 Vector2 lastTargetPosition;
85 float cooldown;
86 float damage;
87 } Tower;
88
89 typedef struct GameTime
90 {
91 float time;
92 float deltaTime;
93 uint32_t frameCount;
94
95 float fixedDeltaTime;
96 // leaving the fixed time stepping to the update functions,
97 // we need to know the fixed time at the start of the frame
98 float fixedTimeStart;
99 // and the number of fixed steps that we have to make this frame
100 // The fixedTime is fixedTimeStart + n * fixedStepCount
101 uint8_t fixedStepCount;
102 } GameTime;
103
104 typedef struct ButtonState {
105 char isSelected;
106 char isDisabled;
107 } ButtonState;
108
109 typedef struct GUIState {
110 int isBlocked;
111 } GUIState;
112
113 typedef enum LevelState
114 {
115 LEVEL_STATE_NONE,
116 LEVEL_STATE_BUILDING,
117 LEVEL_STATE_BUILDING_PLACEMENT,
118 LEVEL_STATE_BATTLE,
119 LEVEL_STATE_WON_WAVE,
120 LEVEL_STATE_LOST_WAVE,
121 LEVEL_STATE_WON_LEVEL,
122 LEVEL_STATE_RESET,
123 } LevelState;
124
125 typedef struct EnemyWave {
126 uint8_t enemyType;
127 uint8_t wave;
128 uint16_t count;
129 float interval;
130 float delay;
131 Vector2 spawnPosition;
132
133 uint16_t spawned;
134 float timeToSpawnNext;
135 } EnemyWave;
136
137 #define ENEMY_MAX_WAVE_COUNT 10
138
139 typedef enum PlacementPhase
140 {
141 PLACEMENT_PHASE_STARTING,
142 PLACEMENT_PHASE_MOVING,
143 PLACEMENT_PHASE_PLACING,
144 } PlacementPhase;
145
146 typedef struct Level
147 {
148 int seed;
149 LevelState state;
150 LevelState nextState;
151 Camera3D camera;
152 int placementMode;
153 PlacementPhase placementPhase;
154 float placementTimer;
155
156 int16_t placementX;
157 int16_t placementY;
158 int8_t placementContextMenuStatus;
159 int8_t placementContextMenuType;
160
161 Vector2 placementTransitionPosition;
162 PhysicsPoint placementTowerSpring;
163
164 int initialGold;
165 int playerGold;
166
167 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
168 int currentWave;
169 float waveEndTimer;
170 } Level;
171
172 typedef struct DeltaSrc
173 {
174 char x, y;
175 } DeltaSrc;
176
177 typedef struct PathfindingMap
178 {
179 int width, height;
180 float scale;
181 float *distances;
182 long *towerIndex;
183 DeltaSrc *deltaSrc;
184 float maxDistance;
185 Matrix toMapSpace;
186 Matrix toWorldSpace;
187 } PathfindingMap;
188
189 // when we execute the pathfinding algorithm, we need to store the active nodes
190 // in a queue. Each node has a position, a distance from the start, and the
191 // position of the node that we came from.
192 typedef struct PathfindingNode
193 {
194 int16_t x, y, fromX, fromY;
195 float distance;
196 } PathfindingNode;
197
198 typedef struct EnemyId
199 {
200 uint16_t index;
201 uint16_t generation;
202 } EnemyId;
203
204 typedef struct EnemyClassConfig
205 {
206 float speed;
207 float health;
208 float shieldHealth;
209 float shieldDamageAbsorption;
210 float radius;
211 float maxAcceleration;
212 float requiredContactTime;
213 float explosionDamage;
214 float explosionRange;
215 float explosionPushbackPower;
216 int goldValue;
217 } EnemyClassConfig;
218
219 typedef struct Enemy
220 {
221 int16_t currentX, currentY;
222 int16_t nextX, nextY;
223 Vector2 simPosition;
224 Vector2 simVelocity;
225 uint16_t generation;
226 float walkedDistance;
227 float startMovingTime;
228 float damage, futureDamage;
229 float shieldDamage;
230 float contactTime;
231 uint8_t enemyType;
232 uint8_t movePathCount;
233 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
234 } Enemy;
235
236 // a unit that uses sprites to be drawn
237 #define SPRITE_UNIT_ANIMATION_COUNT 6
238 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
239 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
240 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
241
242 typedef struct SpriteAnimation
243 {
244 Rectangle srcRect;
245 Vector2 offset;
246 uint8_t animationId;
247 uint8_t frameCount;
248 uint8_t frameWidth;
249 float frameDuration;
250 } SpriteAnimation;
251
252 typedef struct SpriteUnit
253 {
254 float scale;
255 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
256 } SpriteUnit;
257
258 #define PROJECTILE_MAX_COUNT 1200
259 #define PROJECTILE_TYPE_NONE 0
260 #define PROJECTILE_TYPE_ARROW 1
261 #define PROJECTILE_TYPE_CATAPULT 2
262 #define PROJECTILE_TYPE_BALLISTA 3
263
264 typedef struct Projectile
265 {
266 uint8_t projectileType;
267 float shootTime;
268 float arrivalTime;
269 float distance;
270 Vector3 position;
271 Vector3 target;
272 Vector3 directionNormal;
273 EnemyId targetEnemy;
274 HitEffectConfig hitEffectConfig;
275 } Projectile;
276
277 //# Function declarations
278 float TowerGetMaxHealth(Tower *tower);
279 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
280 int EnemyAddDamageRange(Vector2 position, float range, float damage);
281 int EnemyAddDamage(Enemy *enemy, float damage);
282
283 //# Enemy functions
284 void EnemyInit();
285 void EnemyDraw();
286 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
287 void EnemyUpdate();
288 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
289 float EnemyGetMaxHealth(Enemy *enemy);
290 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
291 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
292 EnemyId EnemyGetId(Enemy *enemy);
293 Enemy *EnemyTryResolve(EnemyId enemyId);
294 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
295 int EnemyAddDamage(Enemy *enemy, float damage);
296 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
297 int EnemyCount();
298 void EnemyDrawHealthbars(Camera3D camera);
299
300 //# Tower functions
301 void TowerInit();
302 Tower *TowerGetAt(int16_t x, int16_t y);
303 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
304 Tower *GetTowerByType(uint8_t towerType);
305 int GetTowerCosts(uint8_t towerType);
306 float TowerGetMaxHealth(Tower *tower);
307 void TowerDraw();
308 void TowerDrawSingle(Tower tower);
309 void TowerUpdate();
310 void TowerDrawHealthBars(Camera3D camera);
311 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
312
313 //# Particles
314 void ParticleInit();
315 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
316 void ParticleUpdate();
317 void ParticleDraw();
318
319 //# Projectiles
320 void ProjectileInit();
321 void ProjectileDraw();
322 void ProjectileUpdate();
323 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
324
325 //# Pathfinding map
326 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
327 float PathFindingGetDistance(int mapX, int mapY);
328 Vector2 PathFindingGetGradient(Vector3 world);
329 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
330 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
331 void PathFindingMapDraw();
332
333 //# UI
334 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
335
336 //# Level
337 void DrawLevelGround(Level *level);
338 void DrawEnemyPath(Level *level, Color arrowColor);
339
340 //# variables
341 extern Level *currentLevel;
342 extern Enemy enemies[ENEMY_MAX_COUNT];
343 extern int enemyCount;
344 extern EnemyClassConfig enemyClassConfigs[];
345
346 extern GUIState guiState;
347 extern GameTime gameTime;
348 extern Tower towers[TOWER_MAX_COUNT];
349 extern int towerCount;
350
351 extern Texture2D palette, spriteSheet;
352
353 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 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 #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 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
It mostly looks and feels the same now, but there is now a small instruction in place to draw the context menu and handle its various options:
- Repair button action does not close the context menu
- Sell / build buttons close the context menu
- Clicking outside the context menu closes it
The next step is to implement different submenus for the tower upgrades. Let's start with the sell confirmation dialog:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6 #include <string.h>
7
8 //# Variables
9 GUIState guiState = {0};
10 GameTime gameTime = {
11 .fixedDeltaTime = 1.0f / 60.0f,
12 };
13
14 Model floorTileAModel = {0};
15 Model floorTileBModel = {0};
16 Model treeModel[2] = {0};
17 Model firTreeModel[2] = {0};
18 Model rockModels[5] = {0};
19 Model grassPatchModel[1] = {0};
20
21 Model pathArrowModel = {0};
22 Model greenArrowModel = {0};
23
24 Texture2D palette, spriteSheet;
25
26 Level levels[] = {
27 [0] = {
28 .state = LEVEL_STATE_BUILDING,
29 .initialGold = 20,
30 .waves[0] = {
31 .enemyType = ENEMY_TYPE_SHIELD,
32 .wave = 0,
33 .count = 1,
34 .interval = 2.5f,
35 .delay = 1.0f,
36 .spawnPosition = {2, 6},
37 },
38 .waves[1] = {
39 .enemyType = ENEMY_TYPE_RUNNER,
40 .wave = 0,
41 .count = 5,
42 .interval = 0.5f,
43 .delay = 1.0f,
44 .spawnPosition = {-2, 6},
45 },
46 .waves[2] = {
47 .enemyType = ENEMY_TYPE_SHIELD,
48 .wave = 1,
49 .count = 20,
50 .interval = 1.5f,
51 .delay = 1.0f,
52 .spawnPosition = {0, 6},
53 },
54 .waves[3] = {
55 .enemyType = ENEMY_TYPE_MINION,
56 .wave = 2,
57 .count = 30,
58 .interval = 1.2f,
59 .delay = 1.0f,
60 .spawnPosition = {2, 6},
61 },
62 .waves[4] = {
63 .enemyType = ENEMY_TYPE_BOSS,
64 .wave = 2,
65 .count = 2,
66 .interval = 5.0f,
67 .delay = 2.0f,
68 .spawnPosition = {-2, 4},
69 }
70 },
71 };
72
73 Level *currentLevel = levels;
74
75 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
76
77 //# Game
78
79 static Model LoadGLBModel(char *filename)
80 {
81 Model model = LoadModel(TextFormat("data/%s.glb",filename));
82 for (int i = 0; i < model.materialCount; i++)
83 {
84 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
85 }
86 return model;
87 }
88
89 void LoadAssets()
90 {
91 // load a sprite sheet that contains all units
92 spriteSheet = LoadTexture("data/spritesheet.png");
93 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
94
95 // we'll use a palette texture to colorize the all buildings and environment art
96 palette = LoadTexture("data/palette.png");
97 // The texture uses gradients on very small space, so we'll enable bilinear filtering
98 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
99
100 floorTileAModel = LoadGLBModel("floor-tile-a");
101 floorTileBModel = LoadGLBModel("floor-tile-b");
102 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
103 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
104 firTreeModel[0] = LoadGLBModel("firtree-1-a");
105 firTreeModel[1] = LoadGLBModel("firtree-1-b");
106 rockModels[0] = LoadGLBModel("rock-1");
107 rockModels[1] = LoadGLBModel("rock-2");
108 rockModels[2] = LoadGLBModel("rock-3");
109 rockModels[3] = LoadGLBModel("rock-4");
110 rockModels[4] = LoadGLBModel("rock-5");
111 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
112
113 pathArrowModel = LoadGLBModel("direction-arrow-x");
114 greenArrowModel = LoadGLBModel("green-arrow");
115 }
116
117 void InitLevel(Level *level)
118 {
119 level->seed = (int)(GetTime() * 100.0f);
120
121 TowerInit();
122 EnemyInit();
123 ProjectileInit();
124 ParticleInit();
125 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
126
127 level->placementMode = 0;
128 level->state = LEVEL_STATE_BUILDING;
129 level->nextState = LEVEL_STATE_NONE;
130 level->playerGold = level->initialGold;
131 level->currentWave = 0;
132 level->placementX = -1;
133 level->placementY = 0;
134
135 Camera *camera = &level->camera;
136 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
137 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
138 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
139 camera->fovy = 11.5f;
140 camera->projection = CAMERA_ORTHOGRAPHIC;
141 }
142
143 void DrawLevelHud(Level *level)
144 {
145 const char *text = TextFormat("Gold: %d", level->playerGold);
146 Font font = GetFontDefault();
147 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
148 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
149 }
150
151 void DrawLevelReportLostWave(Level *level)
152 {
153 BeginMode3D(level->camera);
154 DrawLevelGround(level);
155 TowerDraw();
156 EnemyDraw();
157 ProjectileDraw();
158 ParticleDraw();
159 guiState.isBlocked = 0;
160 EndMode3D();
161
162 TowerDrawHealthBars(level->camera);
163
164 const char *text = "Wave lost";
165 int textWidth = MeasureText(text, 20);
166 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
167
168 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
169 {
170 level->nextState = LEVEL_STATE_RESET;
171 }
172 }
173
174 int HasLevelNextWave(Level *level)
175 {
176 for (int i = 0; i < 10; i++)
177 {
178 EnemyWave *wave = &level->waves[i];
179 if (wave->wave == level->currentWave)
180 {
181 return 1;
182 }
183 }
184 return 0;
185 }
186
187 void DrawLevelReportWonWave(Level *level)
188 {
189 BeginMode3D(level->camera);
190 DrawLevelGround(level);
191 TowerDraw();
192 EnemyDraw();
193 ProjectileDraw();
194 ParticleDraw();
195 guiState.isBlocked = 0;
196 EndMode3D();
197
198 TowerDrawHealthBars(level->camera);
199
200 const char *text = "Wave won";
201 int textWidth = MeasureText(text, 20);
202 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
203
204
205 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
206 {
207 level->nextState = LEVEL_STATE_RESET;
208 }
209
210 if (HasLevelNextWave(level))
211 {
212 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
213 {
214 level->nextState = LEVEL_STATE_BUILDING;
215 }
216 }
217 else {
218 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
219 {
220 level->nextState = LEVEL_STATE_WON_LEVEL;
221 }
222 }
223 }
224
225 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
226 {
227 static ButtonState buttonStates[8] = {0};
228 int cost = GetTowerCosts(towerType);
229 const char *text = TextFormat("%s: %d", name, cost);
230 buttonStates[towerType].isSelected = level->placementMode == towerType;
231 buttonStates[towerType].isDisabled = level->playerGold < cost;
232 if (Button(text, x, y, width, height, &buttonStates[towerType]))
233 {
234 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
235 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
236 return 1;
237 }
238 return 0;
239 }
240
241 float GetRandomFloat(float min, float max)
242 {
243 int random = GetRandomValue(0, 0xfffffff);
244 return ((float)random / (float)0xfffffff) * (max - min) + min;
245 }
246
247 void DrawLevelGround(Level *level)
248 {
249 // draw checkerboard ground pattern
250 for (int x = -5; x <= 5; x += 1)
251 {
252 for (int y = -5; y <= 5; y += 1)
253 {
254 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
255 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
256 }
257 }
258
259 int oldSeed = GetRandomValue(0, 0xfffffff);
260 SetRandomSeed(level->seed);
261 // increase probability for trees via duplicated entries
262 Model borderModels[64];
263 int maxRockCount = GetRandomValue(2, 6);
264 int maxTreeCount = GetRandomValue(10, 20);
265 int maxFirTreeCount = GetRandomValue(5, 10);
266 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
267 int grassPatchCount = GetRandomValue(5, 30);
268
269 int modelCount = 0;
270 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
271 {
272 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
273 }
274 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
275 {
276 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
277 }
278 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
279 {
280 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
281 }
282 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
283 {
284 borderModels[modelCount++] = grassPatchModel[0];
285 }
286
287 // draw some objects around the border of the map
288 Vector3 up = {0, 1, 0};
289 // a pseudo random number generator to get the same result every time
290 const float wiggle = 0.75f;
291 const int layerCount = 3;
292 for (int layer = 0; layer <= layerCount; layer++)
293 {
294 int layerPos = 6 + layer;
295 Model *selectedModels = borderModels;
296 int selectedModelCount = modelCount;
297 if (layer == 0)
298 {
299 selectedModels = grassPatchModel;
300 selectedModelCount = 1;
301 }
302 for (int x = -6 - layer; x <= 6 + layer; x += 1)
303 {
304 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
305 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
306 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
307 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
308 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
309 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
310 }
311
312 for (int z = -5 - layer; z <= 5 + layer; z += 1)
313 {
314 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
315 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
316 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
317 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
318 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
319 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
320 }
321 }
322
323 SetRandomSeed(oldSeed);
324 }
325
326 void DrawEnemyPath(Level *level, Color arrowColor)
327 {
328 const int castleX = 0, castleY = 0;
329 const int maxWaypointCount = 200;
330 const float timeStep = 1.0f;
331 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
332
333 // we start with a time offset to simulate the path,
334 // this way the arrows are animated in a forward moving direction
335 // The time is wrapped around the time step to get a smooth animation
336 float timeOffset = fmodf(GetTime(), timeStep);
337
338 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
339 {
340 EnemyWave *wave = &level->waves[i];
341 if (wave->wave != level->currentWave)
342 {
343 continue;
344 }
345
346 // use this dummy enemy to simulate the path
347 Enemy dummy = {
348 .enemyType = ENEMY_TYPE_MINION,
349 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
350 .nextX = wave->spawnPosition.x,
351 .nextY = wave->spawnPosition.y,
352 .currentX = wave->spawnPosition.x,
353 .currentY = wave->spawnPosition.y,
354 };
355
356 float deltaTime = timeOffset;
357 for (int j = 0; j < maxWaypointCount; j++)
358 {
359 int waypointPassedCount = 0;
360 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
361 // after the initial variable starting offset, we use a fixed time step
362 deltaTime = timeStep;
363 dummy.simPosition = pos;
364
365 // Update the dummy's position just like we do in the regular enemy update loop
366 for (int k = 0; k < waypointPassedCount; k++)
367 {
368 dummy.currentX = dummy.nextX;
369 dummy.currentY = dummy.nextY;
370 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
371 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
372 {
373 break;
374 }
375 }
376 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
377 {
378 break;
379 }
380
381 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
382 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
383 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
384 }
385 }
386 }
387
388 void DrawEnemyPaths(Level *level)
389 {
390 // disable depth testing for the path arrows
391 // flush the 3D batch to draw the arrows on top of everything
392 rlDrawRenderBatchActive();
393 rlDisableDepthTest();
394 DrawEnemyPath(level, (Color){64, 64, 64, 160});
395
396 rlDrawRenderBatchActive();
397 rlEnableDepthTest();
398 DrawEnemyPath(level, WHITE);
399 }
400
401 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
402 {
403 float dt = gameTime.fixedDeltaTime;
404 // smooth transition for the placement position using exponential decay
405 const float lambda = 15.0f;
406 float factor = 1.0f - expf(-lambda * dt);
407
408 float damping = 0.5f;
409 float springStiffness = 300.0f;
410 float springDecay = 95.0f;
411 float minHeight = 0.35f;
412
413 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
414 {
415 damping = 1.0f;
416 springDecay = 90.0f;
417 springStiffness = 100.0f;
418 minHeight = 0.70f;
419 }
420
421 for (int i = 0; i < gameTime.fixedStepCount; i++)
422 {
423 level->placementTransitionPosition =
424 Vector2Lerp(
425 level->placementTransitionPosition,
426 (Vector2){mapX, mapY}, factor);
427
428 // draw the spring position for debugging the spring simulation
429 // first step: stiff spring, no simulation
430 Vector3 worldPlacementPosition = (Vector3){
431 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
432 Vector3 springTargetPosition = (Vector3){
433 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
434 // consider the current velocity to predict the future position in order to dampen
435 // the spring simulation. Longer prediction times will result in more damping
436 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
437 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
438 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
439 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
440 // decay velocity of the upright forcing spring
441 // This force acts like a 2nd spring that pulls the tip upright into the air above the
442 // base position
443 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
444 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
445
446 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
447 // we use a simple spring model with a rest length of 1.0f
448 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
449 float springLength = Vector3Length(springDelta);
450 float springForce = (springLength - 1.0f) * springStiffness;
451 Vector3 springForceVector = Vector3Normalize(springDelta);
452 springForceVector = Vector3Scale(springForceVector, springForce);
453 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
454 Vector3Scale(springForceVector, dt));
455
456 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
457 Vector3Scale(level->placementTowerSpring.velocity, dt));
458 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
459 {
460 level->placementTowerSpring.velocity.y *= -1.0f;
461 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
462 }
463 }
464 }
465
466 void DrawLevelBuildingPlacementState(Level *level)
467 {
468 const float placementDuration = 0.5f;
469
470 level->placementTimer += gameTime.deltaTime;
471 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
472 {
473 level->placementPhase = PLACEMENT_PHASE_MOVING;
474 level->placementTimer = 0.0f;
475 }
476
477 BeginMode3D(level->camera);
478 DrawLevelGround(level);
479
480 int blockedCellCount = 0;
481 Vector2 blockedCells[1];
482 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
483 float planeDistance = ray.position.y / -ray.direction.y;
484 float planeX = ray.direction.x * planeDistance + ray.position.x;
485 float planeY = ray.direction.z * planeDistance + ray.position.z;
486 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
487 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
488 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
489 level->placementMode && !guiState.isBlocked &&
490 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
491 {
492 level->placementX = mapX;
493 level->placementY = mapY;
494 }
495 else
496 {
497 mapX = level->placementX;
498 mapY = level->placementY;
499 }
500 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
501 PathFindingMapUpdate(blockedCellCount, blockedCells);
502
503 TowerDraw();
504 EnemyDraw();
505 ProjectileDraw();
506 ParticleDraw();
507 DrawEnemyPaths(level);
508
509 // let the tower float up and down. Consider this height in the spring simulation as well
510 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
511
512 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
513 {
514 // The bouncing spring needs a bit of outro time to look nice and complete.
515 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
516 // and the last 1/3rd is the outro physics (bouncing)
517 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
518 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
519 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
520 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
521 towerFloatHeight = linearBlendHeight + parabola;
522 }
523
524 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
525
526 rlPushMatrix();
527 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
528
529 rlPushMatrix();
530 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
531 // calculate x and z rotation to align the model with the spring
532 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
533 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
534 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
535 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
536 float springLength = Vector3Length(towerUp);
537 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
538 float towerSquash = 1.0f / towerStretch;
539 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
540 rlScalef(towerSquash, towerStretch, towerSquash);
541 Tower dummy = {
542 .towerType = level->placementMode,
543 };
544 TowerDrawSingle(dummy);
545 rlPopMatrix();
546
547 // draw a shadow for the tower
548 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
549 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
550 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
551
552
553 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
554 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
555 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
556 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
557
558 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
559 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
560 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
561 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
562 rlPopMatrix();
563
564 guiState.isBlocked = 0;
565
566 EndMode3D();
567
568 TowerDrawHealthBars(level->camera);
569
570 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
571 {
572 if (level->placementTimer > placementDuration)
573 {
574 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
575 // testing repairing
576 tower->damage = 2.5f;
577 level->playerGold -= GetTowerCosts(level->placementMode);
578 level->nextState = LEVEL_STATE_BUILDING;
579 level->placementMode = TOWER_TYPE_NONE;
580 }
581 }
582 else
583 {
584 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
585 {
586 level->nextState = LEVEL_STATE_BUILDING;
587 level->placementMode = TOWER_TYPE_NONE;
588 TraceLog(LOG_INFO, "Cancel building");
589 }
590
591 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
592 {
593 level->placementPhase = PLACEMENT_PHASE_PLACING;
594 level->placementTimer = 0.0f;
595 }
596 }
597 }
598
599 enum ContextMenuType
600 {
601 CONTEXT_MENU_TYPE_MAIN,
602 CONTEXT_MENU_TYPE_SELL_CONFIRM,
603 };
604
605 typedef struct ContextMenuArgs
606 {
607 void *data;
608 uint8_t uint8;
609 int32_t int32;
610 Tower *tower;
611 } ContextMenuArgs;
612
613 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
614 {
615 uint8_t towerType = data->uint8;
616 level->placementMode = towerType;
617 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
618 return 1;
619 }
620
621 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
622 {
623 Tower *tower = data->tower;
624 int gold = data->int32;
625 level->playerGold += gold;
626 tower->towerType = TOWER_TYPE_NONE;
627 return 1;
628 }
629
630 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
631 {
632 return 1;
633 }
634
635 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
636 {
637 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
638 return 0;
639 }
640
641 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
642 {
643 Tower *tower = data->tower;
644 if (level->playerGold >= 1)
645 {
646 level->playerGold -= 1;
647 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
648 }
649 return tower->damage == 0.0f;
650 }
651
652 typedef struct ContextMenuItem
653 {
654 uint8_t index;
655 char text[24];
656 float alignX;
657 int (*action)(Level*, ContextMenuArgs*);
658 void *data;
659 ContextMenuArgs args;
660 ButtonState buttonState;
661 } ContextMenuItem;
662
663 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
664 {
665 ContextMenuItem item = {.index = index, .alignX = alignX};
666 strncpy(item.text, text, 24);
667 return item;
668 }
669
670 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
671 {
672 ContextMenuItem item = {.index = index, .action = action, .args = args};
673 strncpy(item.text, text, 24);
674 return item;
675 }
676
677 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
678 {
679 const int itemHeight = 28;
680 const int itemSpacing = 4;
681 int itemCount = 0;
682 for (int i = 0; menus[i].text[0]; i++)
683 {
684 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
685 }
686
687 Rectangle contextMenu = {0, 0, width,
688 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing};
689
690 Vector2 anchor = anchorHigh.y > contextMenu.height ? anchorHigh : anchorLow;
691 float anchorPivotY = anchorHigh.y > contextMenu.height ? 1.0f : 0.0f;
692
693 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
694 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
695 DrawRectangle(contextMenu.x, contextMenu.y, contextMenu.width, contextMenu.height, (Color){0, 0, 0, 128});
696 const int itemX = contextMenu.x + itemSpacing;
697 const int itemWidth = contextMenu.width - itemSpacing * 2;
698 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx)
699 #define ITEM_RECT(idx, marginLR) itemX + (marginLR), ITEM_Y(idx), itemWidth - (marginLR) * 2, itemHeight
700 int status = 0;
701 for (int i = 0; menus[i].text[0]; i++)
702 {
703 if (menus[i].action)
704 {
705 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
706 {
707 status = menus[i].action(level, &menus[i].args);
708 }
709 }
710 else
711 {
712 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
713 }
714 }
715
716 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
717 {
718 return 1;
719 }
720
721 return status;
722 }
723
724 void DrawLevelBuildingState(Level *level)
725 {
726 BeginMode3D(level->camera);
727 DrawLevelGround(level);
728
729 PathFindingMapUpdate(0, 0);
730 TowerDraw();
731 EnemyDraw();
732 ProjectileDraw();
733 ParticleDraw();
734 DrawEnemyPaths(level);
735
736 guiState.isBlocked = 0;
737
738 // when the context menu is not active, we update the placement position
739 if (level->placementContextMenuStatus == 0)
740 {
741 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
742 float hitDistance = ray.position.y / -ray.direction.y;
743 float hitX = ray.direction.x * hitDistance + ray.position.x;
744 float hitY = ray.direction.z * hitDistance + ray.position.z;
745 level->placementX = (int)floorf(hitX + 0.5f);
746 level->placementY = (int)floorf(hitY + 0.5f);
747 }
748
749 // Hover rectangle, when the mouse is over the map
750 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
751 if (isHovering)
752 {
753 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
754 }
755
756 EndMode3D();
757
758 TowerDrawHealthBars(level->camera);
759
760 // Draw the context menu when the context menu is active
761 if (level->placementContextMenuStatus >= 1)
762 {
763 Tower *tower = TowerGetAt(level->placementX, level->placementY);
764 float maxHitpoints = 0.0f;
765 float hp = 0.0f;
766 float damageFactor = 0.0f;
767 int32_t sellValue = 0;
768
769 if (tower)
770 {
771 maxHitpoints = TowerGetMaxHealth(tower);
772 hp = maxHitpoints - tower->damage;
773 damageFactor = 1.0f - tower->damage / maxHitpoints;
774 sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
775 }
776
777 ContextMenuItem menu[12] = {0};
778 int menuCount = 0;
779 int menuIndex = 0;
780
781 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
782 {
783 if (tower)
784 {
785
786 if (tower) {
787 menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f);
788 }
789
790 // two texts, same line
791 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
792 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
793
794 if (tower->towerType != TOWER_TYPE_BASE)
795 {
796
797 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
798 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
799 }
800 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
801 {
802 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
803 (ContextMenuArgs){.tower = tower});
804 }
805 }
806 else
807 {
808 menu[menuCount] = ContextMenuItemButton(menuIndex++,
809 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)),
810 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
811 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL);
812
813 menu[menuCount] = ContextMenuItemButton(menuIndex++,
814 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)),
815 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
816 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER);
817
818 menu[menuCount] = ContextMenuItemButton(menuIndex++,
819 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)),
820 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
821 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA);
822
823 menu[menuCount] = ContextMenuItemButton(menuIndex++,
824 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)),
825 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
826 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT);
827 }
828
829 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
830 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
831 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
832 {
833 level->placementContextMenuStatus = -1;
834 }
835 }
836 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
837 {
838 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f);
839 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
840 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
841 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
842 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
843 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
844 {
845 level->placementContextMenuStatus = -1;
846 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
847 }
848 }
849 }
850
851 // Activate the context menu when the mouse is clicked and the context menu is not active
852 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
853 {
854 level->placementContextMenuStatus += 1;
855 }
856
857 // undefine the macros so we don't cause trouble in other functions
858 #undef ITEM_Y
859 #undef ITEM_RECT
860
861
862 if (level->placementContextMenuStatus == 0)
863 {
864 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
865 {
866 level->nextState = LEVEL_STATE_RESET;
867 }
868
869 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
870 {
871 level->nextState = LEVEL_STATE_BATTLE;
872 }
873
874 const char *text = "Building phase";
875 int textWidth = MeasureText(text, 20);
876 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 10, 20, WHITE);
877 }
878
879 }
880
881 void InitBattleStateConditions(Level *level)
882 {
883 level->state = LEVEL_STATE_BATTLE;
884 level->nextState = LEVEL_STATE_NONE;
885 level->waveEndTimer = 0.0f;
886 for (int i = 0; i < 10; i++)
887 {
888 EnemyWave *wave = &level->waves[i];
889 wave->spawned = 0;
890 wave->timeToSpawnNext = wave->delay;
891 }
892 }
893
894 void DrawLevelBattleState(Level *level)
895 {
896 BeginMode3D(level->camera);
897 DrawLevelGround(level);
898 TowerDraw();
899 EnemyDraw();
900 ProjectileDraw();
901 ParticleDraw();
902 guiState.isBlocked = 0;
903 EndMode3D();
904
905 EnemyDrawHealthbars(level->camera);
906 TowerDrawHealthBars(level->camera);
907
908 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
909 {
910 level->nextState = LEVEL_STATE_RESET;
911 }
912
913 int maxCount = 0;
914 int remainingCount = 0;
915 for (int i = 0; i < 10; i++)
916 {
917 EnemyWave *wave = &level->waves[i];
918 if (wave->wave != level->currentWave)
919 {
920 continue;
921 }
922 maxCount += wave->count;
923 remainingCount += wave->count - wave->spawned;
924 }
925 int aliveCount = EnemyCount();
926 remainingCount += aliveCount;
927
928 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
929 int textWidth = MeasureText(text, 20);
930 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
931 }
932
933 void DrawLevel(Level *level)
934 {
935 switch (level->state)
936 {
937 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
938 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
939 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
940 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
941 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
942 default: break;
943 }
944
945 DrawLevelHud(level);
946 }
947
948 void UpdateLevel(Level *level)
949 {
950 if (level->state == LEVEL_STATE_BATTLE)
951 {
952 int activeWaves = 0;
953 for (int i = 0; i < 10; i++)
954 {
955 EnemyWave *wave = &level->waves[i];
956 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
957 {
958 continue;
959 }
960 activeWaves++;
961 wave->timeToSpawnNext -= gameTime.deltaTime;
962 if (wave->timeToSpawnNext <= 0.0f)
963 {
964 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
965 if (enemy)
966 {
967 wave->timeToSpawnNext = wave->interval;
968 wave->spawned++;
969 }
970 }
971 }
972 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
973 level->waveEndTimer += gameTime.deltaTime;
974 if (level->waveEndTimer >= 2.0f)
975 {
976 level->nextState = LEVEL_STATE_LOST_WAVE;
977 }
978 }
979 else if (activeWaves == 0 && EnemyCount() == 0)
980 {
981 level->waveEndTimer += gameTime.deltaTime;
982 if (level->waveEndTimer >= 2.0f)
983 {
984 level->nextState = LEVEL_STATE_WON_WAVE;
985 }
986 }
987 }
988
989 PathFindingMapUpdate(0, 0);
990 EnemyUpdate();
991 TowerUpdate();
992 ProjectileUpdate();
993 ParticleUpdate();
994
995 if (level->nextState == LEVEL_STATE_RESET)
996 {
997 InitLevel(level);
998 }
999
1000 if (level->nextState == LEVEL_STATE_BATTLE)
1001 {
1002 InitBattleStateConditions(level);
1003 }
1004
1005 if (level->nextState == LEVEL_STATE_WON_WAVE)
1006 {
1007 level->currentWave++;
1008 level->state = LEVEL_STATE_WON_WAVE;
1009 }
1010
1011 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1012 {
1013 level->state = LEVEL_STATE_LOST_WAVE;
1014 }
1015
1016 if (level->nextState == LEVEL_STATE_BUILDING)
1017 {
1018 level->state = LEVEL_STATE_BUILDING;
1019 level->placementContextMenuStatus = 0;
1020 }
1021
1022 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1023 {
1024 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1025 level->placementTransitionPosition = (Vector2){
1026 level->placementX, level->placementY};
1027 // initialize the spring to the current position
1028 level->placementTowerSpring = (PhysicsPoint){
1029 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1030 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1031 };
1032 level->placementPhase = PLACEMENT_PHASE_STARTING;
1033 level->placementTimer = 0.0f;
1034 }
1035
1036 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1037 {
1038 // make something of this later
1039 InitLevel(level);
1040 }
1041
1042 level->nextState = LEVEL_STATE_NONE;
1043 }
1044
1045 float nextSpawnTime = 0.0f;
1046
1047 void ResetGame()
1048 {
1049 InitLevel(currentLevel);
1050 }
1051
1052 void InitGame()
1053 {
1054 TowerInit();
1055 EnemyInit();
1056 ProjectileInit();
1057 ParticleInit();
1058 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1059
1060 currentLevel = levels;
1061 InitLevel(currentLevel);
1062 }
1063
1064 //# Immediate GUI functions
1065
1066 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1067 {
1068 const float healthBarHeight = 6.0f;
1069 const float healthBarOffset = 15.0f;
1070 const float inset = 2.0f;
1071 const float innerWidth = healthBarWidth - inset * 2;
1072 const float innerHeight = healthBarHeight - inset * 2;
1073
1074 Vector2 screenPos = GetWorldToScreen(position, camera);
1075 screenPos = Vector2Add(screenPos, screenOffset);
1076 float centerX = screenPos.x - healthBarWidth * 0.5f;
1077 float topY = screenPos.y - healthBarOffset;
1078 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1079 float healthWidth = innerWidth * healthRatio;
1080 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1081 }
1082
1083 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1084 {
1085 Font font = GetFontDefault();
1086 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1087
1088 DrawTextEx(font, text, (Vector2){
1089 x + (width - textSize.x) * alignX,
1090 y + (height - textSize.y) * alignY
1091 }, font.baseSize * 2.0f, 1, textColor);
1092 }
1093
1094 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1095 {
1096 Rectangle bounds = {x, y, width, height};
1097 int isPressed = 0;
1098 int isSelected = state && state->isSelected;
1099 int isDisabled = state && state->isDisabled;
1100 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1101 {
1102 Color color = isSelected ? DARKGRAY : GRAY;
1103 DrawRectangle(x, y, width, height, color);
1104 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1105 {
1106 isPressed = 1;
1107 }
1108 guiState.isBlocked = 1;
1109 }
1110 else
1111 {
1112 Color color = isSelected ? WHITE : LIGHTGRAY;
1113 DrawRectangle(x, y, width, height, color);
1114 }
1115 Font font = GetFontDefault();
1116 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1117 Color textColor = isDisabled ? GRAY : BLACK;
1118 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
1119 return isPressed;
1120 }
1121
1122 //# Main game loop
1123
1124 void GameUpdate()
1125 {
1126 UpdateLevel(currentLevel);
1127 }
1128
1129 int main(void)
1130 {
1131 int screenWidth, screenHeight;
1132 GetPreferredSize(&screenWidth, &screenHeight);
1133 InitWindow(screenWidth, screenHeight, "Tower defense");
1134 float gamespeed = 1.0f;
1135 SetTargetFPS(30);
1136
1137 LoadAssets();
1138 InitGame();
1139
1140 float pause = 1.0f;
1141
1142 while (!WindowShouldClose())
1143 {
1144 if (IsPaused()) {
1145 // canvas is not visible in browser - do nothing
1146 continue;
1147 }
1148
1149 if (IsKeyPressed(KEY_T))
1150 {
1151 gamespeed += 0.1f;
1152 if (gamespeed > 1.05f) gamespeed = 0.1f;
1153 }
1154
1155 if (IsKeyPressed(KEY_P))
1156 {
1157 pause = pause > 0.5f ? 0.0f : 1.0f;
1158 }
1159
1160 float dt = GetFrameTime() * gamespeed * pause;
1161 // cap maximum delta time to 0.1 seconds to prevent large time steps
1162 if (dt > 0.1f) dt = 0.1f;
1163 gameTime.time += dt;
1164 gameTime.deltaTime = dt;
1165 gameTime.frameCount += 1;
1166
1167 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1168 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1169
1170 BeginDrawing();
1171 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1172
1173 GameUpdate();
1174 DrawLevel(currentLevel);
1175
1176 if (gamespeed != 1.0f)
1177 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1178 EndDrawing();
1179
1180 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1181 }
1182
1183 CloseWindow();
1184
1185 return 0;
1186 }
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 const char *name;
62 float cooldown;
63 float range;
64 float projectileSpeed;
65
66 uint8_t cost;
67 uint8_t projectileType;
68 uint16_t maxHealth;
69
70 HitEffectConfig hitEffect;
71 } TowerTypeConfig;
72
73 typedef struct TowerUpgradeState
74 {
75 uint8_t range;
76 uint8_t damage;
77 uint8_t speed;
78 } TowerUpgradeState;
79
80 typedef struct Tower
81 {
82 int16_t x, y;
83 uint8_t towerType;
84 TowerUpgradeState upgradeState;
85 Vector2 lastTargetPosition;
86 float cooldown;
87 float damage;
88 } Tower;
89
90 typedef struct GameTime
91 {
92 float time;
93 float deltaTime;
94 uint32_t frameCount;
95
96 float fixedDeltaTime;
97 // leaving the fixed time stepping to the update functions,
98 // we need to know the fixed time at the start of the frame
99 float fixedTimeStart;
100 // and the number of fixed steps that we have to make this frame
101 // The fixedTime is fixedTimeStart + n * fixedStepCount
102 uint8_t fixedStepCount;
103 } GameTime;
104
105 typedef struct ButtonState {
106 char isSelected;
107 char isDisabled;
108 } ButtonState;
109
110 typedef struct GUIState {
111 int isBlocked;
112 } GUIState;
113
114 typedef enum LevelState
115 {
116 LEVEL_STATE_NONE,
117 LEVEL_STATE_BUILDING,
118 LEVEL_STATE_BUILDING_PLACEMENT,
119 LEVEL_STATE_BATTLE,
120 LEVEL_STATE_WON_WAVE,
121 LEVEL_STATE_LOST_WAVE,
122 LEVEL_STATE_WON_LEVEL,
123 LEVEL_STATE_RESET,
124 } LevelState;
125
126 typedef struct EnemyWave {
127 uint8_t enemyType;
128 uint8_t wave;
129 uint16_t count;
130 float interval;
131 float delay;
132 Vector2 spawnPosition;
133
134 uint16_t spawned;
135 float timeToSpawnNext;
136 } EnemyWave;
137
138 #define ENEMY_MAX_WAVE_COUNT 10
139
140 typedef enum PlacementPhase
141 {
142 PLACEMENT_PHASE_STARTING,
143 PLACEMENT_PHASE_MOVING,
144 PLACEMENT_PHASE_PLACING,
145 } PlacementPhase;
146
147 typedef struct Level
148 {
149 int seed;
150 LevelState state;
151 LevelState nextState;
152 Camera3D camera;
153 int placementMode;
154 PlacementPhase placementPhase;
155 float placementTimer;
156
157 int16_t placementX;
158 int16_t placementY;
159 int8_t placementContextMenuStatus;
160 int8_t placementContextMenuType;
161
162 Vector2 placementTransitionPosition;
163 PhysicsPoint placementTowerSpring;
164
165 int initialGold;
166 int playerGold;
167
168 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
169 int currentWave;
170 float waveEndTimer;
171 } Level;
172
173 typedef struct DeltaSrc
174 {
175 char x, y;
176 } DeltaSrc;
177
178 typedef struct PathfindingMap
179 {
180 int width, height;
181 float scale;
182 float *distances;
183 long *towerIndex;
184 DeltaSrc *deltaSrc;
185 float maxDistance;
186 Matrix toMapSpace;
187 Matrix toWorldSpace;
188 } PathfindingMap;
189
190 // when we execute the pathfinding algorithm, we need to store the active nodes
191 // in a queue. Each node has a position, a distance from the start, and the
192 // position of the node that we came from.
193 typedef struct PathfindingNode
194 {
195 int16_t x, y, fromX, fromY;
196 float distance;
197 } PathfindingNode;
198
199 typedef struct EnemyId
200 {
201 uint16_t index;
202 uint16_t generation;
203 } EnemyId;
204
205 typedef struct EnemyClassConfig
206 {
207 float speed;
208 float health;
209 float shieldHealth;
210 float shieldDamageAbsorption;
211 float radius;
212 float maxAcceleration;
213 float requiredContactTime;
214 float explosionDamage;
215 float explosionRange;
216 float explosionPushbackPower;
217 int goldValue;
218 } EnemyClassConfig;
219
220 typedef struct Enemy
221 {
222 int16_t currentX, currentY;
223 int16_t nextX, nextY;
224 Vector2 simPosition;
225 Vector2 simVelocity;
226 uint16_t generation;
227 float walkedDistance;
228 float startMovingTime;
229 float damage, futureDamage;
230 float shieldDamage;
231 float contactTime;
232 uint8_t enemyType;
233 uint8_t movePathCount;
234 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
235 } Enemy;
236
237 // a unit that uses sprites to be drawn
238 #define SPRITE_UNIT_ANIMATION_COUNT 6
239 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
240 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
241 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
242
243 typedef struct SpriteAnimation
244 {
245 Rectangle srcRect;
246 Vector2 offset;
247 uint8_t animationId;
248 uint8_t frameCount;
249 uint8_t frameWidth;
250 float frameDuration;
251 } SpriteAnimation;
252
253 typedef struct SpriteUnit
254 {
255 float scale;
256 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
257 } SpriteUnit;
258
259 #define PROJECTILE_MAX_COUNT 1200
260 #define PROJECTILE_TYPE_NONE 0
261 #define PROJECTILE_TYPE_ARROW 1
262 #define PROJECTILE_TYPE_CATAPULT 2
263 #define PROJECTILE_TYPE_BALLISTA 3
264
265 typedef struct Projectile
266 {
267 uint8_t projectileType;
268 float shootTime;
269 float arrivalTime;
270 float distance;
271 Vector3 position;
272 Vector3 target;
273 Vector3 directionNormal;
274 EnemyId targetEnemy;
275 HitEffectConfig hitEffectConfig;
276 } Projectile;
277
278 //# Function declarations
279 float TowerGetMaxHealth(Tower *tower);
280 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
281 int EnemyAddDamageRange(Vector2 position, float range, float damage);
282 int EnemyAddDamage(Enemy *enemy, float damage);
283
284 //# Enemy functions
285 void EnemyInit();
286 void EnemyDraw();
287 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
288 void EnemyUpdate();
289 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
290 float EnemyGetMaxHealth(Enemy *enemy);
291 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
292 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
293 EnemyId EnemyGetId(Enemy *enemy);
294 Enemy *EnemyTryResolve(EnemyId enemyId);
295 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
296 int EnemyAddDamage(Enemy *enemy, float damage);
297 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
298 int EnemyCount();
299 void EnemyDrawHealthbars(Camera3D camera);
300
301 //# Tower functions
302 void TowerInit();
303 Tower *TowerGetAt(int16_t x, int16_t y);
304 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
305 Tower *GetTowerByType(uint8_t towerType);
306 int GetTowerCosts(uint8_t towerType);
307 const char *GetTowerName(uint8_t towerType);
308 float TowerGetMaxHealth(Tower *tower);
309 void TowerDraw();
310 void TowerDrawSingle(Tower tower);
311 void TowerUpdate();
312 void TowerDrawHealthBars(Camera3D camera);
313 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
314
315 //# Particles
316 void ParticleInit();
317 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
318 void ParticleUpdate();
319 void ParticleDraw();
320
321 //# Projectiles
322 void ProjectileInit();
323 void ProjectileDraw();
324 void ProjectileUpdate();
325 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
326
327 //# Pathfinding map
328 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
329 float PathFindingGetDistance(int mapX, int mapY);
330 Vector2 PathFindingGetGradient(Vector3 world);
331 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
332 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
333 void PathFindingMapDraw();
334
335 //# UI
336 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
337
338 //# Level
339 void DrawLevelGround(Level *level);
340 void DrawEnemyPath(Level *level, Color arrowColor);
341
342 //# variables
343 extern Level *currentLevel;
344 extern Enemy enemies[ENEMY_MAX_COUNT];
345 extern int enemyCount;
346 extern EnemyClassConfig enemyClassConfigs[];
347
348 extern GUIState guiState;
349 extern GameTime gameTime;
350 extern Tower towers[TOWER_MAX_COUNT];
351 extern int towerCount;
352
353 extern Texture2D palette, spriteSheet;
354
355 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .name = "Castle",
7 .maxHealth = 10,
8 },
9 [TOWER_TYPE_ARCHER] = {
10 .name = "Archer",
11 .cooldown = 0.5f,
12 .range = 3.0f,
13 .cost = 6,
14 .maxHealth = 10,
15 .projectileSpeed = 4.0f,
16 .projectileType = PROJECTILE_TYPE_ARROW,
17 .hitEffect = {
18 .damage = 3.0f,
19 }
20 },
21 [TOWER_TYPE_BALLISTA] = {
22 .name = "Ballista",
23 .cooldown = 1.5f,
24 .range = 6.0f,
25 .cost = 9,
26 .maxHealth = 10,
27 .projectileSpeed = 10.0f,
28 .projectileType = PROJECTILE_TYPE_BALLISTA,
29 .hitEffect = {
30 .damage = 8.0f,
31 .pushbackPowerDistance = 0.25f,
32 }
33 },
34 [TOWER_TYPE_CATAPULT] = {
35 .name = "Catapult",
36 .cooldown = 1.7f,
37 .range = 5.0f,
38 .cost = 10,
39 .maxHealth = 10,
40 .projectileSpeed = 3.0f,
41 .projectileType = PROJECTILE_TYPE_CATAPULT,
42 .hitEffect = {
43 .damage = 2.0f,
44 .areaDamageRadius = 1.75f,
45 }
46 },
47 [TOWER_TYPE_WALL] = {
48 .name = "Wall",
49 .cost = 2,
50 .maxHealth = 10,
51 },
52 };
53
54 Tower towers[TOWER_MAX_COUNT];
55 int towerCount = 0;
56
57 Model towerModels[TOWER_TYPE_COUNT];
58
59 // definition of our archer unit
60 SpriteUnit archerUnit = {
61 .animations[0] = {
62 .srcRect = {0, 0, 16, 16},
63 .offset = {7, 1},
64 .frameCount = 1,
65 .frameDuration = 0.0f,
66 },
67 .animations[1] = {
68 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
69 .srcRect = {16, 0, 6, 16},
70 .offset = {8, 0},
71 },
72 .animations[2] = {
73 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
74 .srcRect = {22, 0, 11, 16},
75 .offset = {10, 0},
76 },
77 };
78
79 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
80 {
81 float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
82 float xScale = flip ? -1.0f : 1.0f;
83 Camera3D camera = currentLevel->camera;
84 float size = 0.5f * unitScale;
85 // we want the sprite to face the camera, so we need to calculate the up vector
86 Vector3 forward = Vector3Subtract(camera.target, camera.position);
87 Vector3 up = {0, 1, 0};
88 Vector3 right = Vector3CrossProduct(forward, up);
89 up = Vector3Normalize(Vector3CrossProduct(right, forward));
90
91 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
92 {
93 SpriteAnimation anim = unit.animations[i];
94 if (anim.animationId != phase && anim.animationId != 0)
95 {
96 continue;
97 }
98 Rectangle srcRect = anim.srcRect;
99 if (anim.frameCount > 1)
100 {
101 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
102 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
103 }
104 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
105 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
106
107 if (flip)
108 {
109 srcRect.x += srcRect.width;
110 srcRect.width = -srcRect.width;
111 offset.x = scale.x - offset.x;
112 }
113 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
114 // move the sprite slightly towards the camera to avoid z-fighting
115 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
116 }
117 }
118
119 void TowerInit()
120 {
121 for (int i = 0; i < TOWER_MAX_COUNT; i++)
122 {
123 towers[i] = (Tower){0};
124 }
125 towerCount = 0;
126
127 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
128 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
129
130 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
131 {
132 if (towerModels[i].materials)
133 {
134 // assign the palette texture to the material of the model (0 is not used afaik)
135 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
136 }
137 }
138 }
139
140 static void TowerGunUpdate(Tower *tower)
141 {
142 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
143 if (tower->cooldown <= 0.0f)
144 {
145 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
146 if (enemy)
147 {
148 tower->cooldown = config.cooldown;
149 // shoot the enemy; determine future position of the enemy
150 float bulletSpeed = config.projectileSpeed;
151 Vector2 velocity = enemy->simVelocity;
152 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
153 Vector2 towerPosition = {tower->x, tower->y};
154 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
155 for (int i = 0; i < 8; i++) {
156 velocity = enemy->simVelocity;
157 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
158 float distance = Vector2Distance(towerPosition, futurePosition);
159 float eta2 = distance / bulletSpeed;
160 if (fabs(eta - eta2) < 0.01f) {
161 break;
162 }
163 eta = (eta2 + eta) * 0.5f;
164 }
165
166 ProjectileTryAdd(config.projectileType, enemy,
167 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
168 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
169 bulletSpeed, config.hitEffect);
170 enemy->futureDamage += config.hitEffect.damage;
171 tower->lastTargetPosition = futurePosition;
172 }
173 }
174 else
175 {
176 tower->cooldown -= gameTime.deltaTime;
177 }
178 }
179
180 Tower *TowerGetAt(int16_t x, int16_t y)
181 {
182 for (int i = 0; i < towerCount; i++)
183 {
184 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
185 {
186 return &towers[i];
187 }
188 }
189 return 0;
190 }
191
192 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
193 {
194 if (towerCount >= TOWER_MAX_COUNT)
195 {
196 return 0;
197 }
198
199 Tower *tower = TowerGetAt(x, y);
200 if (tower)
201 {
202 return 0;
203 }
204
205 tower = &towers[towerCount++];
206 tower->x = x;
207 tower->y = y;
208 tower->towerType = towerType;
209 tower->cooldown = 0.0f;
210 tower->damage = 0.0f;
211 return tower;
212 }
213
214 Tower *GetTowerByType(uint8_t towerType)
215 {
216 for (int i = 0; i < towerCount; i++)
217 {
218 if (towers[i].towerType == towerType)
219 {
220 return &towers[i];
221 }
222 }
223 return 0;
224 }
225
226 const char *GetTowerName(uint8_t towerType)
227 {
228 return towerTypeConfigs[towerType].name;
229 }
230
231 int GetTowerCosts(uint8_t towerType)
232 {
233 return towerTypeConfigs[towerType].cost;
234 }
235
236 float TowerGetMaxHealth(Tower *tower)
237 {
238 return towerTypeConfigs[tower->towerType].maxHealth;
239 }
240
241 void TowerDrawSingle(Tower tower)
242 {
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 return;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275
276 void TowerDraw()
277 {
278 for (int i = 0; i < towerCount; i++)
279 {
280 TowerDrawSingle(towers[i]);
281 }
282 }
283
284 void TowerUpdate()
285 {
286 for (int i = 0; i < towerCount; i++)
287 {
288 Tower *tower = &towers[i];
289 switch (tower->towerType)
290 {
291 case TOWER_TYPE_CATAPULT:
292 case TOWER_TYPE_BALLISTA:
293 case TOWER_TYPE_ARCHER:
294 TowerGunUpdate(tower);
295 break;
296 }
297 }
298 }
299
300 void TowerDrawHealthBars(Camera3D camera)
301 {
302 for (int i = 0; i < towerCount; i++)
303 {
304 Tower *tower = &towers[i];
305 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
306 {
307 continue;
308 }
309
310 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
311 float maxHealth = TowerGetMaxHealth(tower);
312 float health = maxHealth - tower->damage;
313 float healthRatio = health / maxHealth;
314
315 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
316 }
317 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 8.0f,
32 .speed = 0.5f,
33 .radius = 0.25f,
34 .maxAcceleration = 1.0f,
35 .explosionDamage = 2.0f,
36 .requiredContactTime = 0.5f,
37 .explosionRange = 1.0f,
38 .explosionPushbackPower = 0.25f,
39 .goldValue = 3,
40 .shieldDamageAbsorption = 4.0f,
41 .shieldHealth = 25.0f,
42 },
43 [ENEMY_TYPE_BOSS] = {
44 .health = 50.0f,
45 .speed = 0.4f,
46 .radius = 0.25f,
47 .maxAcceleration = 1.0f,
48 .explosionDamage = 5.0f,
49 .requiredContactTime = 0.5f,
50 .explosionRange = 1.0f,
51 .explosionPushbackPower = 0.25f,
52 .goldValue = 10,
53 },
54 };
55
56 Enemy enemies[ENEMY_MAX_COUNT];
57 int enemyCount = 0;
58
59 SpriteUnit enemySprites[] = {
60 [ENEMY_TYPE_MINION] = {
61 .animations[0] = {
62 .srcRect = {0, 17, 16, 15},
63 .offset = {8.0f, 0.0f},
64 .frameCount = 6,
65 .frameDuration = 0.1f,
66 },
67 .animations[1] = {
68 .srcRect = {1, 33, 15, 14},
69 .offset = {7.0f, 0.0f},
70 .frameCount = 6,
71 .frameWidth = 16,
72 .frameDuration = 0.1f,
73 },
74 },
75 [ENEMY_TYPE_RUNNER] = {
76 .scale = 0.75f,
77 .animations[0] = {
78 .srcRect = {0, 17, 16, 15},
79 .offset = {8.0f, 0.0f},
80 .frameCount = 6,
81 .frameDuration = 0.1f,
82 },
83 },
84 [ENEMY_TYPE_SHIELD] = {
85 .animations[0] = {
86 .srcRect = {0, 17, 16, 15},
87 .offset = {8.0f, 0.0f},
88 .frameCount = 6,
89 .frameDuration = 0.1f,
90 },
91 .animations[1] = {
92 .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
93 .srcRect = {99, 17, 10, 11},
94 .offset = {7.0f, 0.0f},
95 },
96 },
97 [ENEMY_TYPE_BOSS] = {
98 .scale = 1.5f,
99 .animations[0] = {
100 .srcRect = {0, 17, 16, 15},
101 .offset = {8.0f, 0.0f},
102 .frameCount = 6,
103 .frameDuration = 0.1f,
104 },
105 .animations[1] = {
106 .srcRect = {97, 29, 14, 7},
107 .offset = {7.0f, -9.0f},
108 },
109 },
110 };
111
112 void EnemyInit()
113 {
114 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115 {
116 enemies[i] = (Enemy){0};
117 }
118 enemyCount = 0;
119 }
120
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123 return enemyClassConfigs[enemy->enemyType].speed;
124 }
125
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128 return enemyClassConfigs[enemy->enemyType].health;
129 }
130
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133 int16_t castleX = 0;
134 int16_t castleY = 0;
135 int16_t dx = castleX - currentX;
136 int16_t dy = castleY - currentY;
137 if (dx == 0 && dy == 0)
138 {
139 *nextX = currentX;
140 *nextY = currentY;
141 return 1;
142 }
143 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144
145 if (gradient.x == 0 && gradient.y == 0)
146 {
147 *nextX = currentX;
148 *nextY = currentY;
149 return 1;
150 }
151
152 if (fabsf(gradient.x) > fabsf(gradient.y))
153 {
154 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155 *nextY = currentY;
156 return 0;
157 }
158 *nextX = currentX;
159 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160 return 0;
161 }
162
163
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167 const float pointReachedDistance = 0.25f;
168 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169 const float maxSimStepTime = 0.015625f;
170
171 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173 int16_t nextX = enemy->nextX;
174 int16_t nextY = enemy->nextY;
175 Vector2 position = enemy->simPosition;
176 int passedCount = 0;
177 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178 {
179 float stepTime = fminf(deltaT - t, maxSimStepTime);
180 Vector2 target = (Vector2){nextX, nextY};
181 float speed = Vector2Length(*velocity);
182 // draw the target position for debugging
183 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186 {
187 // we reached the target position, let's move to the next waypoint
188 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189 target = (Vector2){nextX, nextY};
190 // track how many waypoints we passed
191 passedCount++;
192 }
193
194 // acceleration towards the target
195 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197 *velocity = Vector2Add(*velocity, acceleration);
198
199 // limit the speed to the maximum speed
200 if (speed > maxSpeed)
201 {
202 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203 }
204
205 // move the enemy
206 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207 }
208
209 if (waypointPassedCount)
210 {
211 (*waypointPassedCount) = passedCount;
212 }
213
214 return position;
215 }
216
217 void EnemyDraw()
218 {
219 rlDrawRenderBatchActive();
220 rlDisableDepthMask();
221 for (int i = 0; i < enemyCount; i++)
222 {
223 Enemy enemy = enemies[i];
224 if (enemy.enemyType == ENEMY_TYPE_NONE)
225 {
226 continue;
227 }
228
229 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230
231 // don't draw any trails for now; might replace this with footprints later
232 // if (enemy.movePathCount > 0)
233 // {
234 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236 // }
237 // for (int j = 1; j < enemy.movePathCount; j++)
238 // {
239 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241 // DrawLine3D(p, q, GREEN);
242 // }
243
244 float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245 int phase = 0;
246 if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247 {
248 phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249 }
250
251 switch (enemy.enemyType)
252 {
253 case ENEMY_TYPE_MINION:
254 case ENEMY_TYPE_RUNNER:
255 case ENEMY_TYPE_SHIELD:
256 case ENEMY_TYPE_BOSS:
257 DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y},
258 enemy.walkedDistance, 0, phase);
259 break;
260 }
261 }
262 rlDrawRenderBatchActive();
263 rlEnableDepthMask();
264 }
265
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268 // damage the tower
269 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272 float explosionRange2 = explosionRange * explosionRange;
273 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274 // explode the enemy
275 if (tower->damage >= TowerGetMaxHealth(tower))
276 {
277 tower->towerType = TOWER_TYPE_NONE;
278 }
279
280 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
281 explosionSource,
282 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283
284 enemy->enemyType = ENEMY_TYPE_NONE;
285
286 // push back enemies & dealing damage
287 for (int i = 0; i < enemyCount; i++)
288 {
289 Enemy *other = &enemies[i];
290 if (other->enemyType == ENEMY_TYPE_NONE)
291 {
292 continue;
293 }
294 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295 if (distanceSqr > 0 && distanceSqr < explosionRange2)
296 {
297 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299 EnemyAddDamage(other, explosionDamge);
300 }
301 }
302 }
303
304 void EnemyUpdate()
305 {
306 const float castleX = 0;
307 const float castleY = 0;
308 const float maxPathDistance2 = 0.25f * 0.25f;
309
310 for (int i = 0; i < enemyCount; i++)
311 {
312 Enemy *enemy = &enemies[i];
313 if (enemy->enemyType == ENEMY_TYPE_NONE)
314 {
315 continue;
316 }
317
318 int waypointPassedCount = 0;
319 Vector2 prevPosition = enemy->simPosition;
320 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321 enemy->startMovingTime = gameTime.time;
322 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323 // track path of unit
324 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325 {
326 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327 {
328 enemy->movePath[j] = enemy->movePath[j - 1];
329 }
330 enemy->movePath[0] = enemy->simPosition;
331 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332 {
333 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334 }
335 }
336
337 if (waypointPassedCount > 0)
338 {
339 enemy->currentX = enemy->nextX;
340 enemy->currentY = enemy->nextY;
341 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343 {
344 // enemy reached the castle; remove it
345 enemy->enemyType = ENEMY_TYPE_NONE;
346 continue;
347 }
348 }
349 }
350
351 // handle collisions between enemies
352 for (int i = 0; i < enemyCount - 1; i++)
353 {
354 Enemy *enemyA = &enemies[i];
355 if (enemyA->enemyType == ENEMY_TYPE_NONE)
356 {
357 continue;
358 }
359 for (int j = i + 1; j < enemyCount; j++)
360 {
361 Enemy *enemyB = &enemies[j];
362 if (enemyB->enemyType == ENEMY_TYPE_NONE)
363 {
364 continue;
365 }
366 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369 float radiusSum = radiusA + radiusB;
370 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371 {
372 // collision
373 float distance = sqrtf(distanceSqr);
374 float overlap = radiusSum - distance;
375 // move the enemies apart, but softly; if we have a clog of enemies,
376 // moving them perfectly apart can cause them to jitter
377 float positionCorrection = overlap / 5.0f;
378 Vector2 direction = (Vector2){
379 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383 }
384 }
385 }
386
387 // handle collisions between enemies and towers
388 for (int i = 0; i < enemyCount; i++)
389 {
390 Enemy *enemy = &enemies[i];
391 if (enemy->enemyType == ENEMY_TYPE_NONE)
392 {
393 continue;
394 }
395 enemy->contactTime -= gameTime.deltaTime;
396 if (enemy->contactTime < 0.0f)
397 {
398 enemy->contactTime = 0.0f;
399 }
400
401 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402 // linear search over towers; could be optimized by using path finding tower map,
403 // but for now, we keep it simple
404 for (int j = 0; j < towerCount; j++)
405 {
406 Tower *tower = &towers[j];
407 if (tower->towerType == TOWER_TYPE_NONE)
408 {
409 continue;
410 }
411 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413 if (distanceSqr > combinedRadius * combinedRadius)
414 {
415 continue;
416 }
417 // potential collision; square / circle intersection
418 float dx = tower->x - enemy->simPosition.x;
419 float dy = tower->y - enemy->simPosition.y;
420 float absDx = fabsf(dx);
421 float absDy = fabsf(dy);
422 Vector3 contactPoint = {0};
423 if (absDx <= 0.5f && absDx <= absDy) {
424 // vertical collision; push the enemy out horizontally
425 float overlap = enemyRadius + 0.5f - absDy;
426 if (overlap < 0.0f)
427 {
428 continue;
429 }
430 float direction = dy > 0.0f ? -1.0f : 1.0f;
431 enemy->simPosition.y += direction * overlap;
432 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433 }
434 else if (absDy <= 0.5f && absDy <= absDx)
435 {
436 // horizontal collision; push the enemy out vertically
437 float overlap = enemyRadius + 0.5f - absDx;
438 if (overlap < 0.0f)
439 {
440 continue;
441 }
442 float direction = dx > 0.0f ? -1.0f : 1.0f;
443 enemy->simPosition.x += direction * overlap;
444 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445 }
446 else
447 {
448 // possible collision with a corner
449 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451 float cornerX = tower->x + cornerDX;
452 float cornerY = tower->y + cornerDY;
453 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454 if (cornerDistanceSqr > enemyRadius * enemyRadius)
455 {
456 continue;
457 }
458 // push the enemy out along the diagonal
459 float cornerDistance = sqrtf(cornerDistanceSqr);
460 float overlap = enemyRadius - cornerDistance;
461 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463 enemy->simPosition.x -= directionX * overlap;
464 enemy->simPosition.y -= directionY * overlap;
465 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466 }
467
468 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469 {
470 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472 {
473 EnemyTriggerExplode(enemy, tower, contactPoint);
474 }
475 }
476 }
477 }
478 }
479
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482 return (EnemyId){enemy - enemies, enemy->generation};
483 }
484
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487 if (enemyId.index >= ENEMY_MAX_COUNT)
488 {
489 return 0;
490 }
491 Enemy *enemy = &enemies[enemyId.index];
492 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493 {
494 return 0;
495 }
496 return enemy;
497 }
498
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501 Enemy *spawn = 0;
502 for (int i = 0; i < enemyCount; i++)
503 {
504 Enemy *enemy = &enemies[i];
505 if (enemy->enemyType == ENEMY_TYPE_NONE)
506 {
507 spawn = enemy;
508 break;
509 }
510 }
511
512 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513 {
514 spawn = &enemies[enemyCount++];
515 }
516
517 if (spawn)
518 {
519 *spawn = (Enemy){
520 .currentX = currentX,
521 .currentY = currentY,
522 .nextX = currentX,
523 .nextY = currentY,
524 .simPosition = (Vector2){currentX, currentY},
525 .simVelocity = (Vector2){0, 0},
526 .enemyType = enemyType,
527 .startMovingTime = gameTime.time,
528 .movePathCount = 0,
529 .walkedDistance = 0.0f,
530 .shieldDamage = 0.0f,
531 .damage = 0.0f,
532 .futureDamage = 0.0f,
533 .contactTime = 0.0f,
534 .generation = spawn->generation + 1,
535 };
536 }
537
538 return spawn;
539 }
540
541 int EnemyAddDamageRange(Vector2 position, float range, float damage)
542 {
543 int count = 0;
544 float range2 = range * range;
545 for (int i = 0; i < enemyCount; i++)
546 {
547 Enemy *enemy = &enemies[i];
548 if (enemy->enemyType == ENEMY_TYPE_NONE)
549 {
550 continue;
551 }
552 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
553 if (distance2 <= range2)
554 {
555 EnemyAddDamage(enemy, damage);
556 count++;
557 }
558 }
559 return count;
560 }
561
562 int EnemyAddDamage(Enemy *enemy, float damage)
563 {
564 float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
565 if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
566 {
567 float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
568 float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
569 enemy->shieldDamage += shieldDamage;
570 damage -= shieldDamage;
571 }
572 enemy->damage += damage;
573 if (enemy->damage >= EnemyGetMaxHealth(enemy))
574 {
575 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
576 enemy->enemyType = ENEMY_TYPE_NONE;
577 return 1;
578 }
579
580 return 0;
581 }
582
583 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
584 {
585 int16_t castleX = 0;
586 int16_t castleY = 0;
587 Enemy* closest = 0;
588 int16_t closestDistance = 0;
589 float range2 = range * range;
590 for (int i = 0; i < enemyCount; i++)
591 {
592 Enemy* enemy = &enemies[i];
593 if (enemy->enemyType == ENEMY_TYPE_NONE)
594 {
595 continue;
596 }
597 float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
598 if (enemy->futureDamage >= maxHealth)
599 {
600 // ignore enemies that will die soon
601 continue;
602 }
603 int16_t dx = castleX - enemy->currentX;
604 int16_t dy = castleY - enemy->currentY;
605 int16_t distance = abs(dx) + abs(dy);
606 if (!closest || distance < closestDistance)
607 {
608 float tdx = towerX - enemy->currentX;
609 float tdy = towerY - enemy->currentY;
610 float tdistance2 = tdx * tdx + tdy * tdy;
611 if (tdistance2 <= range2)
612 {
613 closest = enemy;
614 closestDistance = distance;
615 }
616 }
617 }
618 return closest;
619 }
620
621 int EnemyCount()
622 {
623 int count = 0;
624 for (int i = 0; i < enemyCount; i++)
625 {
626 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
627 {
628 count++;
629 }
630 }
631 return count;
632 }
633
634 void EnemyDrawHealthbars(Camera3D camera)
635 {
636 for (int i = 0; i < enemyCount; i++)
637 {
638 Enemy *enemy = &enemies[i];
639
640 float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
641 if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
642 {
643 float shieldHealth = maxShieldHealth - enemy->shieldDamage;
644 float shieldHealthRatio = shieldHealth / maxShieldHealth;
645 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
646 DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
647 }
648
649 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
650 {
651 continue;
652 }
653 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
654 float maxHealth = EnemyGetMaxHealth(enemy);
655 float health = maxHealth - enemy->damage;
656 float healthRatio = health / maxHealth;
657
658 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
659 }
660 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
The sell confirmation dialog is a simple yes/no dialog:
It appears that the generic context menu handling function can also be used for simple dialogue boxes. We could even improve the function to allow multiple buttons in one line, essentially creating a simple layouting system. But for now, let's keep it simple.
What would be nice to have is to make the UI a little more appealing.
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6 #include <string.h>
7
8 //# Variables
9 Font gameFontNormal = {0};
10 GUIState guiState = {0};
11 GameTime gameTime = {
12 .fixedDeltaTime = 1.0f / 60.0f,
13 };
14
15 Model floorTileAModel = {0};
16 Model floorTileBModel = {0};
17 Model treeModel[2] = {0};
18 Model firTreeModel[2] = {0};
19 Model rockModels[5] = {0};
20 Model grassPatchModel[1] = {0};
21
22 Model pathArrowModel = {0};
23 Model greenArrowModel = {0};
24
25 Texture2D palette, spriteSheet;
26
27 NPatchInfo uiPanelPatch = {
28 .layout = NPATCH_NINE_PATCH,
29 .source = {145, 1, 46, 46},
30 .top = 18, .bottom = 18,
31 .left = 16, .right = 16
32 };
33 NPatchInfo uiButtonNormal = {
34 .layout = NPATCH_NINE_PATCH,
35 .source = {193, 1, 32, 20},
36 .top = 7, .bottom = 7,
37 .left = 10, .right = 10
38 };
39 NPatchInfo uiButtonDisabled = {
40 .layout = NPATCH_NINE_PATCH,
41 .source = {193, 22, 32, 20},
42 .top = 7, .bottom = 7,
43 .left = 10, .right = 10
44 };
45 NPatchInfo uiButtonHovered = {
46 .layout = NPATCH_NINE_PATCH,
47 .source = {193, 43, 32, 20},
48 .top = 7, .bottom = 7,
49 .left = 10, .right = 10
50 };
51 NPatchInfo uiButtonPressed = {
52 .layout = NPATCH_NINE_PATCH,
53 .source = {193, 64, 32, 20},
54 .top = 7, .bottom = 7,
55 .left = 10, .right = 10
56 };
57 Rectangle uiDiamondMarker = {145, 48, 15, 15};
58
59 Level levels[] = {
60 [0] = {
61 .state = LEVEL_STATE_BUILDING,
62 .initialGold = 20,
63 .waves[0] = {
64 .enemyType = ENEMY_TYPE_SHIELD,
65 .wave = 0,
66 .count = 1,
67 .interval = 2.5f,
68 .delay = 1.0f,
69 .spawnPosition = {2, 6},
70 },
71 .waves[1] = {
72 .enemyType = ENEMY_TYPE_RUNNER,
73 .wave = 0,
74 .count = 5,
75 .interval = 0.5f,
76 .delay = 1.0f,
77 .spawnPosition = {-2, 6},
78 },
79 .waves[2] = {
80 .enemyType = ENEMY_TYPE_SHIELD,
81 .wave = 1,
82 .count = 20,
83 .interval = 1.5f,
84 .delay = 1.0f,
85 .spawnPosition = {0, 6},
86 },
87 .waves[3] = {
88 .enemyType = ENEMY_TYPE_MINION,
89 .wave = 2,
90 .count = 30,
91 .interval = 1.2f,
92 .delay = 1.0f,
93 .spawnPosition = {2, 6},
94 },
95 .waves[4] = {
96 .enemyType = ENEMY_TYPE_BOSS,
97 .wave = 2,
98 .count = 2,
99 .interval = 5.0f,
100 .delay = 2.0f,
101 .spawnPosition = {-2, 4},
102 }
103 },
104 };
105
106 Level *currentLevel = levels;
107
108 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
109
110 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
111 {
112 int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
113 int panelWidth = textWidth + 40;
114 int posX = anchorX - panelWidth * alignX;
115 int textOffset = 20;
116 DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
117 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
118 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
119 }
120
121 void DrawTitle(const char *text)
122 {
123 DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
124 }
125
126 //# Game
127
128 static Model LoadGLBModel(char *filename)
129 {
130 Model model = LoadModel(TextFormat("data/%s.glb",filename));
131 for (int i = 0; i < model.materialCount; i++)
132 {
133 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
134 }
135 return model;
136 }
137
138 void LoadAssets()
139 {
140 // load a sprite sheet that contains all units
141 spriteSheet = LoadTexture("data/spritesheet.png");
142 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
143
144 // we'll use a palette texture to colorize the all buildings and environment art
145 palette = LoadTexture("data/palette.png");
146 // The texture uses gradients on very small space, so we'll enable bilinear filtering
147 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
148
149 gameFontNormal = LoadFont("data/alagard.png");
150
151 floorTileAModel = LoadGLBModel("floor-tile-a");
152 floorTileBModel = LoadGLBModel("floor-tile-b");
153 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
154 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
155 firTreeModel[0] = LoadGLBModel("firtree-1-a");
156 firTreeModel[1] = LoadGLBModel("firtree-1-b");
157 rockModels[0] = LoadGLBModel("rock-1");
158 rockModels[1] = LoadGLBModel("rock-2");
159 rockModels[2] = LoadGLBModel("rock-3");
160 rockModels[3] = LoadGLBModel("rock-4");
161 rockModels[4] = LoadGLBModel("rock-5");
162 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
163
164 pathArrowModel = LoadGLBModel("direction-arrow-x");
165 greenArrowModel = LoadGLBModel("green-arrow");
166 }
167
168 void InitLevel(Level *level)
169 {
170 level->seed = (int)(GetTime() * 100.0f);
171
172 TowerInit();
173 EnemyInit();
174 ProjectileInit();
175 ParticleInit();
176 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
177
178 level->placementMode = 0;
179 level->state = LEVEL_STATE_BUILDING;
180 level->nextState = LEVEL_STATE_NONE;
181 level->playerGold = level->initialGold;
182 level->currentWave = 0;
183 level->placementX = -1;
184 level->placementY = 0;
185
186 Camera *camera = &level->camera;
187 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
188 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
189 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
190 camera->fovy = 11.5f;
191 camera->projection = CAMERA_ORTHOGRAPHIC;
192 }
193
194 void DrawLevelHud(Level *level)
195 {
196 const char *text = TextFormat("Gold: %d", level->playerGold);
197 DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
198 }
199
200 void DrawLevelReportLostWave(Level *level)
201 {
202 BeginMode3D(level->camera);
203 DrawLevelGround(level);
204 TowerDraw();
205 EnemyDraw();
206 ProjectileDraw();
207 ParticleDraw();
208 guiState.isBlocked = 0;
209 EndMode3D();
210
211 TowerDrawHealthBars(level->camera);
212
213 DrawTitle("Wave lost");
214
215 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
216 {
217 level->nextState = LEVEL_STATE_RESET;
218 }
219 }
220
221 int HasLevelNextWave(Level *level)
222 {
223 for (int i = 0; i < 10; i++)
224 {
225 EnemyWave *wave = &level->waves[i];
226 if (wave->wave == level->currentWave)
227 {
228 return 1;
229 }
230 }
231 return 0;
232 }
233
234 void DrawLevelReportWonWave(Level *level)
235 {
236 BeginMode3D(level->camera);
237 DrawLevelGround(level);
238 TowerDraw();
239 EnemyDraw();
240 ProjectileDraw();
241 ParticleDraw();
242 guiState.isBlocked = 0;
243 EndMode3D();
244
245 TowerDrawHealthBars(level->camera);
246
247 DrawTitle("Wave won");
248
249
250 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
251 {
252 level->nextState = LEVEL_STATE_RESET;
253 }
254
255 if (HasLevelNextWave(level))
256 {
257 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
258 {
259 level->nextState = LEVEL_STATE_BUILDING;
260 }
261 }
262 else {
263 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
264 {
265 level->nextState = LEVEL_STATE_WON_LEVEL;
266 }
267 }
268 }
269
270 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
271 {
272 static ButtonState buttonStates[8] = {0};
273 int cost = GetTowerCosts(towerType);
274 const char *text = TextFormat("%s: %d", name, cost);
275 buttonStates[towerType].isSelected = level->placementMode == towerType;
276 buttonStates[towerType].isDisabled = level->playerGold < cost;
277 if (Button(text, x, y, width, height, &buttonStates[towerType]))
278 {
279 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
280 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
281 return 1;
282 }
283 return 0;
284 }
285
286 float GetRandomFloat(float min, float max)
287 {
288 int random = GetRandomValue(0, 0xfffffff);
289 return ((float)random / (float)0xfffffff) * (max - min) + min;
290 }
291
292 void DrawLevelGround(Level *level)
293 {
294 // draw checkerboard ground pattern
295 for (int x = -5; x <= 5; x += 1)
296 {
297 for (int y = -5; y <= 5; y += 1)
298 {
299 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
300 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
301 }
302 }
303
304 int oldSeed = GetRandomValue(0, 0xfffffff);
305 SetRandomSeed(level->seed);
306 // increase probability for trees via duplicated entries
307 Model borderModels[64];
308 int maxRockCount = GetRandomValue(2, 6);
309 int maxTreeCount = GetRandomValue(10, 20);
310 int maxFirTreeCount = GetRandomValue(5, 10);
311 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
312 int grassPatchCount = GetRandomValue(5, 30);
313
314 int modelCount = 0;
315 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
316 {
317 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
318 }
319 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
320 {
321 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
322 }
323 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
324 {
325 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
326 }
327 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
328 {
329 borderModels[modelCount++] = grassPatchModel[0];
330 }
331
332 // draw some objects around the border of the map
333 Vector3 up = {0, 1, 0};
334 // a pseudo random number generator to get the same result every time
335 const float wiggle = 0.75f;
336 const int layerCount = 3;
337 for (int layer = 0; layer <= layerCount; layer++)
338 {
339 int layerPos = 6 + layer;
340 Model *selectedModels = borderModels;
341 int selectedModelCount = modelCount;
342 if (layer == 0)
343 {
344 selectedModels = grassPatchModel;
345 selectedModelCount = 1;
346 }
347 for (int x = -6 - layer; x <= 6 + layer; x += 1)
348 {
349 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
350 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
351 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
352 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
353 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
354 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
355 }
356
357 for (int z = -5 - layer; z <= 5 + layer; z += 1)
358 {
359 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
360 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
361 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
362 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
363 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
364 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
365 }
366 }
367
368 SetRandomSeed(oldSeed);
369 }
370
371 void DrawEnemyPath(Level *level, Color arrowColor)
372 {
373 const int castleX = 0, castleY = 0;
374 const int maxWaypointCount = 200;
375 const float timeStep = 1.0f;
376 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
377
378 // we start with a time offset to simulate the path,
379 // this way the arrows are animated in a forward moving direction
380 // The time is wrapped around the time step to get a smooth animation
381 float timeOffset = fmodf(GetTime(), timeStep);
382
383 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
384 {
385 EnemyWave *wave = &level->waves[i];
386 if (wave->wave != level->currentWave)
387 {
388 continue;
389 }
390
391 // use this dummy enemy to simulate the path
392 Enemy dummy = {
393 .enemyType = ENEMY_TYPE_MINION,
394 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
395 .nextX = wave->spawnPosition.x,
396 .nextY = wave->spawnPosition.y,
397 .currentX = wave->spawnPosition.x,
398 .currentY = wave->spawnPosition.y,
399 };
400
401 float deltaTime = timeOffset;
402 for (int j = 0; j < maxWaypointCount; j++)
403 {
404 int waypointPassedCount = 0;
405 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
406 // after the initial variable starting offset, we use a fixed time step
407 deltaTime = timeStep;
408 dummy.simPosition = pos;
409
410 // Update the dummy's position just like we do in the regular enemy update loop
411 for (int k = 0; k < waypointPassedCount; k++)
412 {
413 dummy.currentX = dummy.nextX;
414 dummy.currentY = dummy.nextY;
415 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
416 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
417 {
418 break;
419 }
420 }
421 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
422 {
423 break;
424 }
425
426 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
427 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
428 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
429 }
430 }
431 }
432
433 void DrawEnemyPaths(Level *level)
434 {
435 // disable depth testing for the path arrows
436 // flush the 3D batch to draw the arrows on top of everything
437 rlDrawRenderBatchActive();
438 rlDisableDepthTest();
439 DrawEnemyPath(level, (Color){64, 64, 64, 160});
440
441 rlDrawRenderBatchActive();
442 rlEnableDepthTest();
443 DrawEnemyPath(level, WHITE);
444 }
445
446 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
447 {
448 float dt = gameTime.fixedDeltaTime;
449 // smooth transition for the placement position using exponential decay
450 const float lambda = 15.0f;
451 float factor = 1.0f - expf(-lambda * dt);
452
453 float damping = 0.5f;
454 float springStiffness = 300.0f;
455 float springDecay = 95.0f;
456 float minHeight = 0.35f;
457
458 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
459 {
460 damping = 1.0f;
461 springDecay = 90.0f;
462 springStiffness = 100.0f;
463 minHeight = 0.70f;
464 }
465
466 for (int i = 0; i < gameTime.fixedStepCount; i++)
467 {
468 level->placementTransitionPosition =
469 Vector2Lerp(
470 level->placementTransitionPosition,
471 (Vector2){mapX, mapY}, factor);
472
473 // draw the spring position for debugging the spring simulation
474 // first step: stiff spring, no simulation
475 Vector3 worldPlacementPosition = (Vector3){
476 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
477 Vector3 springTargetPosition = (Vector3){
478 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
479 // consider the current velocity to predict the future position in order to dampen
480 // the spring simulation. Longer prediction times will result in more damping
481 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
482 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
483 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
484 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
485 // decay velocity of the upright forcing spring
486 // This force acts like a 2nd spring that pulls the tip upright into the air above the
487 // base position
488 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
489 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
490
491 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
492 // we use a simple spring model with a rest length of 1.0f
493 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
494 float springLength = Vector3Length(springDelta);
495 float springForce = (springLength - 1.0f) * springStiffness;
496 Vector3 springForceVector = Vector3Normalize(springDelta);
497 springForceVector = Vector3Scale(springForceVector, springForce);
498 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
499 Vector3Scale(springForceVector, dt));
500
501 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
502 Vector3Scale(level->placementTowerSpring.velocity, dt));
503 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
504 {
505 level->placementTowerSpring.velocity.y *= -1.0f;
506 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
507 }
508 }
509 }
510
511 void DrawLevelBuildingPlacementState(Level *level)
512 {
513 const float placementDuration = 0.5f;
514
515 level->placementTimer += gameTime.deltaTime;
516 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
517 {
518 level->placementPhase = PLACEMENT_PHASE_MOVING;
519 level->placementTimer = 0.0f;
520 }
521
522 BeginMode3D(level->camera);
523 DrawLevelGround(level);
524
525 int blockedCellCount = 0;
526 Vector2 blockedCells[1];
527 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
528 float planeDistance = ray.position.y / -ray.direction.y;
529 float planeX = ray.direction.x * planeDistance + ray.position.x;
530 float planeY = ray.direction.z * planeDistance + ray.position.z;
531 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
532 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
533 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
534 level->placementMode && !guiState.isBlocked &&
535 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
536 {
537 level->placementX = mapX;
538 level->placementY = mapY;
539 }
540 else
541 {
542 mapX = level->placementX;
543 mapY = level->placementY;
544 }
545 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
546 PathFindingMapUpdate(blockedCellCount, blockedCells);
547
548 TowerDraw();
549 EnemyDraw();
550 ProjectileDraw();
551 ParticleDraw();
552 DrawEnemyPaths(level);
553
554 // let the tower float up and down. Consider this height in the spring simulation as well
555 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
556
557 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
558 {
559 // The bouncing spring needs a bit of outro time to look nice and complete.
560 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
561 // and the last 1/3rd is the outro physics (bouncing)
562 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
563 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
564 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
565 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
566 towerFloatHeight = linearBlendHeight + parabola;
567 }
568
569 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
570
571 rlPushMatrix();
572 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
573
574 rlPushMatrix();
575 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
576 // calculate x and z rotation to align the model with the spring
577 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
578 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
579 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
580 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
581 float springLength = Vector3Length(towerUp);
582 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
583 float towerSquash = 1.0f / towerStretch;
584 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
585 rlScalef(towerSquash, towerStretch, towerSquash);
586 Tower dummy = {
587 .towerType = level->placementMode,
588 };
589 TowerDrawSingle(dummy);
590 rlPopMatrix();
591
592 // draw a shadow for the tower
593 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
594 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
595 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
596
597
598 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
599 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
600 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
601 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
602
603 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
604 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
605 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
606 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
607 rlPopMatrix();
608
609 guiState.isBlocked = 0;
610
611 EndMode3D();
612
613 TowerDrawHealthBars(level->camera);
614
615 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
616 {
617 if (level->placementTimer > placementDuration)
618 {
619 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
620 // testing repairing
621 tower->damage = 2.5f;
622 level->playerGold -= GetTowerCosts(level->placementMode);
623 level->nextState = LEVEL_STATE_BUILDING;
624 level->placementMode = TOWER_TYPE_NONE;
625 }
626 }
627 else
628 {
629 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
630 {
631 level->nextState = LEVEL_STATE_BUILDING;
632 level->placementMode = TOWER_TYPE_NONE;
633 TraceLog(LOG_INFO, "Cancel building");
634 }
635
636 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
637 {
638 level->placementPhase = PLACEMENT_PHASE_PLACING;
639 level->placementTimer = 0.0f;
640 }
641 }
642 }
643
644 enum ContextMenuType
645 {
646 CONTEXT_MENU_TYPE_MAIN,
647 CONTEXT_MENU_TYPE_SELL_CONFIRM,
648 };
649
650 typedef struct ContextMenuArgs
651 {
652 void *data;
653 uint8_t uint8;
654 int32_t int32;
655 Tower *tower;
656 } ContextMenuArgs;
657
658 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
659 {
660 uint8_t towerType = data->uint8;
661 level->placementMode = towerType;
662 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
663 return 1;
664 }
665
666 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
667 {
668 Tower *tower = data->tower;
669 int gold = data->int32;
670 level->playerGold += gold;
671 tower->towerType = TOWER_TYPE_NONE;
672 return 1;
673 }
674
675 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
676 {
677 return 1;
678 }
679
680 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
681 {
682 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
683 return 0;
684 }
685
686 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
687 {
688 Tower *tower = data->tower;
689 if (level->playerGold >= 1)
690 {
691 level->playerGold -= 1;
692 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
693 }
694 return tower->damage == 0.0f;
695 }
696
697 typedef struct ContextMenuItem
698 {
699 uint8_t index;
700 char text[24];
701 float alignX;
702 int (*action)(Level*, ContextMenuArgs*);
703 void *data;
704 ContextMenuArgs args;
705 ButtonState buttonState;
706 } ContextMenuItem;
707
708 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
709 {
710 ContextMenuItem item = {.index = index, .alignX = alignX};
711 strncpy(item.text, text, 24);
712 return item;
713 }
714
715 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
716 {
717 ContextMenuItem item = {.index = index, .action = action, .args = args};
718 strncpy(item.text, text, 24);
719 return item;
720 }
721
722 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
723 {
724 const int itemHeight = 28;
725 const int itemSpacing = 1;
726 const int padding = 8;
727 int itemCount = 0;
728 for (int i = 0; menus[i].text[0]; i++)
729 {
730 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
731 }
732
733 Rectangle contextMenu = {0, 0, width,
734 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
735
736 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
737 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
738
739 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
740 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
741 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
742 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
743 const int itemX = contextMenu.x + itemSpacing;
744 const int itemWidth = contextMenu.width - itemSpacing * 2;
745 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
746 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
747 int status = 0;
748 for (int i = 0; menus[i].text[0]; i++)
749 {
750 if (menus[i].action)
751 {
752 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
753 {
754 status = menus[i].action(level, &menus[i].args);
755 }
756 }
757 else
758 {
759 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
760 }
761 }
762
763 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
764 {
765 return 1;
766 }
767
768 return status;
769 }
770
771 void DrawLevelBuildingState(Level *level)
772 {
773 BeginMode3D(level->camera);
774 DrawLevelGround(level);
775
776 PathFindingMapUpdate(0, 0);
777 TowerDraw();
778 EnemyDraw();
779 ProjectileDraw();
780 ParticleDraw();
781 DrawEnemyPaths(level);
782
783 guiState.isBlocked = 0;
784
785 // when the context menu is not active, we update the placement position
786 if (level->placementContextMenuStatus == 0)
787 {
788 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
789 float hitDistance = ray.position.y / -ray.direction.y;
790 float hitX = ray.direction.x * hitDistance + ray.position.x;
791 float hitY = ray.direction.z * hitDistance + ray.position.z;
792 level->placementX = (int)floorf(hitX + 0.5f);
793 level->placementY = (int)floorf(hitY + 0.5f);
794 }
795
796 // Hover rectangle, when the mouse is over the map
797 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
798 if (isHovering)
799 {
800 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
801 }
802
803 EndMode3D();
804
805 TowerDrawHealthBars(level->camera);
806
807 DrawTitle("Building phase");
808
809 // Draw the context menu when the context menu is active
810 if (level->placementContextMenuStatus >= 1)
811 {
812 Tower *tower = TowerGetAt(level->placementX, level->placementY);
813 float maxHitpoints = 0.0f;
814 float hp = 0.0f;
815 float damageFactor = 0.0f;
816 int32_t sellValue = 0;
817
818 if (tower)
819 {
820 maxHitpoints = TowerGetMaxHealth(tower);
821 hp = maxHitpoints - tower->damage;
822 damageFactor = 1.0f - tower->damage / maxHitpoints;
823 sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
824 }
825
826 ContextMenuItem menu[12] = {0};
827 int menuCount = 0;
828 int menuIndex = 0;
829
830 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
831 {
832 if (tower)
833 {
834
835 if (tower) {
836 menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f);
837 }
838
839 // two texts, same line
840 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
841 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
842
843 if (tower->towerType != TOWER_TYPE_BASE)
844 {
845
846 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
847 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
848 }
849 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
850 {
851 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
852 (ContextMenuArgs){.tower = tower});
853 }
854 }
855 else
856 {
857 menu[menuCount] = ContextMenuItemButton(menuIndex++,
858 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)),
859 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
860 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL);
861
862 menu[menuCount] = ContextMenuItemButton(menuIndex++,
863 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)),
864 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
865 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER);
866
867 menu[menuCount] = ContextMenuItemButton(menuIndex++,
868 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)),
869 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
870 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA);
871
872 menu[menuCount] = ContextMenuItemButton(menuIndex++,
873 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)),
874 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
875 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT);
876 }
877
878 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
879 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
880 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
881 {
882 level->placementContextMenuStatus = -1;
883 }
884 }
885 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
886 {
887 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f);
888 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
889 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
890 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
891 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
892 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
893 {
894 level->placementContextMenuStatus = -1;
895 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
896 }
897 }
898 }
899
900 // Activate the context menu when the mouse is clicked and the context menu is not active
901 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
902 {
903 level->placementContextMenuStatus += 1;
904 }
905
906 if (level->placementContextMenuStatus == 0)
907 {
908 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
909 {
910 level->nextState = LEVEL_STATE_RESET;
911 }
912
913 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
914 {
915 level->nextState = LEVEL_STATE_BATTLE;
916 }
917
918 }
919 }
920
921 void InitBattleStateConditions(Level *level)
922 {
923 level->state = LEVEL_STATE_BATTLE;
924 level->nextState = LEVEL_STATE_NONE;
925 level->waveEndTimer = 0.0f;
926 for (int i = 0; i < 10; i++)
927 {
928 EnemyWave *wave = &level->waves[i];
929 wave->spawned = 0;
930 wave->timeToSpawnNext = wave->delay;
931 }
932 }
933
934 void DrawLevelBattleState(Level *level)
935 {
936 BeginMode3D(level->camera);
937 DrawLevelGround(level);
938 TowerDraw();
939 EnemyDraw();
940 ProjectileDraw();
941 ParticleDraw();
942 guiState.isBlocked = 0;
943 EndMode3D();
944
945 EnemyDrawHealthbars(level->camera);
946 TowerDrawHealthBars(level->camera);
947
948 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
949 {
950 level->nextState = LEVEL_STATE_RESET;
951 }
952
953 int maxCount = 0;
954 int remainingCount = 0;
955 for (int i = 0; i < 10; i++)
956 {
957 EnemyWave *wave = &level->waves[i];
958 if (wave->wave != level->currentWave)
959 {
960 continue;
961 }
962 maxCount += wave->count;
963 remainingCount += wave->count - wave->spawned;
964 }
965 int aliveCount = EnemyCount();
966 remainingCount += aliveCount;
967
968 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
969 DrawTitle(text);
970 }
971
972 void DrawLevel(Level *level)
973 {
974 switch (level->state)
975 {
976 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
977 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
978 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
979 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
980 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
981 default: break;
982 }
983
984 DrawLevelHud(level);
985 }
986
987 void UpdateLevel(Level *level)
988 {
989 if (level->state == LEVEL_STATE_BATTLE)
990 {
991 int activeWaves = 0;
992 for (int i = 0; i < 10; i++)
993 {
994 EnemyWave *wave = &level->waves[i];
995 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
996 {
997 continue;
998 }
999 activeWaves++;
1000 wave->timeToSpawnNext -= gameTime.deltaTime;
1001 if (wave->timeToSpawnNext <= 0.0f)
1002 {
1003 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1004 if (enemy)
1005 {
1006 wave->timeToSpawnNext = wave->interval;
1007 wave->spawned++;
1008 }
1009 }
1010 }
1011 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1012 level->waveEndTimer += gameTime.deltaTime;
1013 if (level->waveEndTimer >= 2.0f)
1014 {
1015 level->nextState = LEVEL_STATE_LOST_WAVE;
1016 }
1017 }
1018 else if (activeWaves == 0 && EnemyCount() == 0)
1019 {
1020 level->waveEndTimer += gameTime.deltaTime;
1021 if (level->waveEndTimer >= 2.0f)
1022 {
1023 level->nextState = LEVEL_STATE_WON_WAVE;
1024 }
1025 }
1026 }
1027
1028 PathFindingMapUpdate(0, 0);
1029 EnemyUpdate();
1030 TowerUpdate();
1031 ProjectileUpdate();
1032 ParticleUpdate();
1033
1034 if (level->nextState == LEVEL_STATE_RESET)
1035 {
1036 InitLevel(level);
1037 }
1038
1039 if (level->nextState == LEVEL_STATE_BATTLE)
1040 {
1041 InitBattleStateConditions(level);
1042 }
1043
1044 if (level->nextState == LEVEL_STATE_WON_WAVE)
1045 {
1046 level->currentWave++;
1047 level->state = LEVEL_STATE_WON_WAVE;
1048 }
1049
1050 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1051 {
1052 level->state = LEVEL_STATE_LOST_WAVE;
1053 }
1054
1055 if (level->nextState == LEVEL_STATE_BUILDING)
1056 {
1057 level->state = LEVEL_STATE_BUILDING;
1058 level->placementContextMenuStatus = 0;
1059 }
1060
1061 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1062 {
1063 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1064 level->placementTransitionPosition = (Vector2){
1065 level->placementX, level->placementY};
1066 // initialize the spring to the current position
1067 level->placementTowerSpring = (PhysicsPoint){
1068 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1069 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1070 };
1071 level->placementPhase = PLACEMENT_PHASE_STARTING;
1072 level->placementTimer = 0.0f;
1073 }
1074
1075 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1076 {
1077 // make something of this later
1078 InitLevel(level);
1079 }
1080
1081 level->nextState = LEVEL_STATE_NONE;
1082 }
1083
1084 float nextSpawnTime = 0.0f;
1085
1086 void ResetGame()
1087 {
1088 InitLevel(currentLevel);
1089 }
1090
1091 void InitGame()
1092 {
1093 TowerInit();
1094 EnemyInit();
1095 ProjectileInit();
1096 ParticleInit();
1097 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1098
1099 currentLevel = levels;
1100 InitLevel(currentLevel);
1101 }
1102
1103 //# Immediate GUI functions
1104
1105 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1106 {
1107 const float healthBarHeight = 6.0f;
1108 const float healthBarOffset = 15.0f;
1109 const float inset = 2.0f;
1110 const float innerWidth = healthBarWidth - inset * 2;
1111 const float innerHeight = healthBarHeight - inset * 2;
1112
1113 Vector2 screenPos = GetWorldToScreen(position, camera);
1114 screenPos = Vector2Add(screenPos, screenOffset);
1115 float centerX = screenPos.x - healthBarWidth * 0.5f;
1116 float topY = screenPos.y - healthBarOffset;
1117 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1118 float healthWidth = innerWidth * healthRatio;
1119 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1120 }
1121
1122 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1123 {
1124 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1125
1126 DrawTextEx(gameFontNormal, text, (Vector2){
1127 x + (width - textSize.x) * alignX,
1128 y + (height - textSize.y) * alignY
1129 }, gameFontNormal.baseSize, 1, textColor);
1130 }
1131
1132 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1133 {
1134 Rectangle bounds = {x, y, width, height};
1135 int isPressed = 0;
1136 int isSelected = state && state->isSelected;
1137 int isDisabled = state && state->isDisabled;
1138 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1139 {
1140 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1141 {
1142 isPressed = 1;
1143 }
1144 guiState.isBlocked = 1;
1145 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1146 bounds, Vector2Zero(), 0, WHITE);
1147 }
1148 else
1149 {
1150 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1151 bounds, Vector2Zero(), 0, WHITE);
1152 }
1153 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1154 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1155 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1156 return isPressed;
1157 }
1158
1159 //# Main game loop
1160
1161 void GameUpdate()
1162 {
1163 UpdateLevel(currentLevel);
1164 }
1165
1166 int main(void)
1167 {
1168 int screenWidth, screenHeight;
1169 GetPreferredSize(&screenWidth, &screenHeight);
1170 InitWindow(screenWidth, screenHeight, "Tower defense");
1171 float gamespeed = 1.0f;
1172 SetTargetFPS(30);
1173
1174 LoadAssets();
1175 InitGame();
1176
1177 float pause = 1.0f;
1178
1179 while (!WindowShouldClose())
1180 {
1181 if (IsPaused()) {
1182 // canvas is not visible in browser - do nothing
1183 continue;
1184 }
1185
1186 if (IsKeyPressed(KEY_T))
1187 {
1188 gamespeed += 0.1f;
1189 if (gamespeed > 1.05f) gamespeed = 0.1f;
1190 }
1191
1192 if (IsKeyPressed(KEY_P))
1193 {
1194 pause = pause > 0.5f ? 0.0f : 1.0f;
1195 }
1196
1197 float dt = GetFrameTime() * gamespeed * pause;
1198 // cap maximum delta time to 0.1 seconds to prevent large time steps
1199 if (dt > 0.1f) dt = 0.1f;
1200 gameTime.time += dt;
1201 gameTime.deltaTime = dt;
1202 gameTime.frameCount += 1;
1203
1204 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1205 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1206
1207 BeginDrawing();
1208 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1209
1210 GameUpdate();
1211 DrawLevel(currentLevel);
1212
1213 if (gamespeed != 1.0f)
1214 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1215 EndDrawing();
1216
1217 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1218 }
1219
1220 CloseWindow();
1221
1222 return 0;
1223 }
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 const char *name;
62 float cooldown;
63 float range;
64 float projectileSpeed;
65
66 uint8_t cost;
67 uint8_t projectileType;
68 uint16_t maxHealth;
69
70 HitEffectConfig hitEffect;
71 } TowerTypeConfig;
72
73 typedef struct TowerUpgradeState
74 {
75 uint8_t range;
76 uint8_t damage;
77 uint8_t speed;
78 } TowerUpgradeState;
79
80 typedef struct Tower
81 {
82 int16_t x, y;
83 uint8_t towerType;
84 TowerUpgradeState upgradeState;
85 Vector2 lastTargetPosition;
86 float cooldown;
87 float damage;
88 } Tower;
89
90 typedef struct GameTime
91 {
92 float time;
93 float deltaTime;
94 uint32_t frameCount;
95
96 float fixedDeltaTime;
97 // leaving the fixed time stepping to the update functions,
98 // we need to know the fixed time at the start of the frame
99 float fixedTimeStart;
100 // and the number of fixed steps that we have to make this frame
101 // The fixedTime is fixedTimeStart + n * fixedStepCount
102 uint8_t fixedStepCount;
103 } GameTime;
104
105 typedef struct ButtonState {
106 char isSelected;
107 char isDisabled;
108 } ButtonState;
109
110 typedef struct GUIState {
111 int isBlocked;
112 } GUIState;
113
114 typedef enum LevelState
115 {
116 LEVEL_STATE_NONE,
117 LEVEL_STATE_BUILDING,
118 LEVEL_STATE_BUILDING_PLACEMENT,
119 LEVEL_STATE_BATTLE,
120 LEVEL_STATE_WON_WAVE,
121 LEVEL_STATE_LOST_WAVE,
122 LEVEL_STATE_WON_LEVEL,
123 LEVEL_STATE_RESET,
124 } LevelState;
125
126 typedef struct EnemyWave {
127 uint8_t enemyType;
128 uint8_t wave;
129 uint16_t count;
130 float interval;
131 float delay;
132 Vector2 spawnPosition;
133
134 uint16_t spawned;
135 float timeToSpawnNext;
136 } EnemyWave;
137
138 #define ENEMY_MAX_WAVE_COUNT 10
139
140 typedef enum PlacementPhase
141 {
142 PLACEMENT_PHASE_STARTING,
143 PLACEMENT_PHASE_MOVING,
144 PLACEMENT_PHASE_PLACING,
145 } PlacementPhase;
146
147 typedef struct Level
148 {
149 int seed;
150 LevelState state;
151 LevelState nextState;
152 Camera3D camera;
153 int placementMode;
154 PlacementPhase placementPhase;
155 float placementTimer;
156
157 int16_t placementX;
158 int16_t placementY;
159 int8_t placementContextMenuStatus;
160 int8_t placementContextMenuType;
161
162 Vector2 placementTransitionPosition;
163 PhysicsPoint placementTowerSpring;
164
165 int initialGold;
166 int playerGold;
167
168 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
169 int currentWave;
170 float waveEndTimer;
171 } Level;
172
173 typedef struct DeltaSrc
174 {
175 char x, y;
176 } DeltaSrc;
177
178 typedef struct PathfindingMap
179 {
180 int width, height;
181 float scale;
182 float *distances;
183 long *towerIndex;
184 DeltaSrc *deltaSrc;
185 float maxDistance;
186 Matrix toMapSpace;
187 Matrix toWorldSpace;
188 } PathfindingMap;
189
190 // when we execute the pathfinding algorithm, we need to store the active nodes
191 // in a queue. Each node has a position, a distance from the start, and the
192 // position of the node that we came from.
193 typedef struct PathfindingNode
194 {
195 int16_t x, y, fromX, fromY;
196 float distance;
197 } PathfindingNode;
198
199 typedef struct EnemyId
200 {
201 uint16_t index;
202 uint16_t generation;
203 } EnemyId;
204
205 typedef struct EnemyClassConfig
206 {
207 float speed;
208 float health;
209 float shieldHealth;
210 float shieldDamageAbsorption;
211 float radius;
212 float maxAcceleration;
213 float requiredContactTime;
214 float explosionDamage;
215 float explosionRange;
216 float explosionPushbackPower;
217 int goldValue;
218 } EnemyClassConfig;
219
220 typedef struct Enemy
221 {
222 int16_t currentX, currentY;
223 int16_t nextX, nextY;
224 Vector2 simPosition;
225 Vector2 simVelocity;
226 uint16_t generation;
227 float walkedDistance;
228 float startMovingTime;
229 float damage, futureDamage;
230 float shieldDamage;
231 float contactTime;
232 uint8_t enemyType;
233 uint8_t movePathCount;
234 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
235 } Enemy;
236
237 // a unit that uses sprites to be drawn
238 #define SPRITE_UNIT_ANIMATION_COUNT 6
239 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
240 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
241 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
242
243 typedef struct SpriteAnimation
244 {
245 Rectangle srcRect;
246 Vector2 offset;
247 uint8_t animationId;
248 uint8_t frameCount;
249 uint8_t frameWidth;
250 float frameDuration;
251 } SpriteAnimation;
252
253 typedef struct SpriteUnit
254 {
255 float scale;
256 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
257 } SpriteUnit;
258
259 #define PROJECTILE_MAX_COUNT 1200
260 #define PROJECTILE_TYPE_NONE 0
261 #define PROJECTILE_TYPE_ARROW 1
262 #define PROJECTILE_TYPE_CATAPULT 2
263 #define PROJECTILE_TYPE_BALLISTA 3
264
265 typedef struct Projectile
266 {
267 uint8_t projectileType;
268 float shootTime;
269 float arrivalTime;
270 float distance;
271 Vector3 position;
272 Vector3 target;
273 Vector3 directionNormal;
274 EnemyId targetEnemy;
275 HitEffectConfig hitEffectConfig;
276 } Projectile;
277
278 //# Function declarations
279 float TowerGetMaxHealth(Tower *tower);
280 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
281 int EnemyAddDamageRange(Vector2 position, float range, float damage);
282 int EnemyAddDamage(Enemy *enemy, float damage);
283
284 //# Enemy functions
285 void EnemyInit();
286 void EnemyDraw();
287 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
288 void EnemyUpdate();
289 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
290 float EnemyGetMaxHealth(Enemy *enemy);
291 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
292 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
293 EnemyId EnemyGetId(Enemy *enemy);
294 Enemy *EnemyTryResolve(EnemyId enemyId);
295 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
296 int EnemyAddDamage(Enemy *enemy, float damage);
297 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
298 int EnemyCount();
299 void EnemyDrawHealthbars(Camera3D camera);
300
301 //# Tower functions
302 void TowerInit();
303 Tower *TowerGetAt(int16_t x, int16_t y);
304 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
305 Tower *GetTowerByType(uint8_t towerType);
306 int GetTowerCosts(uint8_t towerType);
307 const char *GetTowerName(uint8_t towerType);
308 float TowerGetMaxHealth(Tower *tower);
309 void TowerDraw();
310 void TowerDrawSingle(Tower tower);
311 void TowerUpdate();
312 void TowerDrawHealthBars(Camera3D camera);
313 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
314
315 //# Particles
316 void ParticleInit();
317 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
318 void ParticleUpdate();
319 void ParticleDraw();
320
321 //# Projectiles
322 void ProjectileInit();
323 void ProjectileDraw();
324 void ProjectileUpdate();
325 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
326
327 //# Pathfinding map
328 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
329 float PathFindingGetDistance(int mapX, int mapY);
330 Vector2 PathFindingGetGradient(Vector3 world);
331 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
332 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
333 void PathFindingMapDraw();
334
335 //# UI
336 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
337
338 //# Level
339 void DrawLevelGround(Level *level);
340 void DrawEnemyPath(Level *level, Color arrowColor);
341
342 //# variables
343 extern Level *currentLevel;
344 extern Enemy enemies[ENEMY_MAX_COUNT];
345 extern int enemyCount;
346 extern EnemyClassConfig enemyClassConfigs[];
347
348 extern GUIState guiState;
349 extern GameTime gameTime;
350 extern Tower towers[TOWER_MAX_COUNT];
351 extern int towerCount;
352
353 extern Texture2D palette, spriteSheet;
354
355 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .name = "Castle",
7 .maxHealth = 10,
8 },
9 [TOWER_TYPE_ARCHER] = {
10 .name = "Archer",
11 .cooldown = 0.5f,
12 .range = 3.0f,
13 .cost = 6,
14 .maxHealth = 10,
15 .projectileSpeed = 4.0f,
16 .projectileType = PROJECTILE_TYPE_ARROW,
17 .hitEffect = {
18 .damage = 3.0f,
19 }
20 },
21 [TOWER_TYPE_BALLISTA] = {
22 .name = "Ballista",
23 .cooldown = 1.5f,
24 .range = 6.0f,
25 .cost = 9,
26 .maxHealth = 10,
27 .projectileSpeed = 10.0f,
28 .projectileType = PROJECTILE_TYPE_BALLISTA,
29 .hitEffect = {
30 .damage = 8.0f,
31 .pushbackPowerDistance = 0.25f,
32 }
33 },
34 [TOWER_TYPE_CATAPULT] = {
35 .name = "Catapult",
36 .cooldown = 1.7f,
37 .range = 5.0f,
38 .cost = 10,
39 .maxHealth = 10,
40 .projectileSpeed = 3.0f,
41 .projectileType = PROJECTILE_TYPE_CATAPULT,
42 .hitEffect = {
43 .damage = 2.0f,
44 .areaDamageRadius = 1.75f,
45 }
46 },
47 [TOWER_TYPE_WALL] = {
48 .name = "Wall",
49 .cost = 2,
50 .maxHealth = 10,
51 },
52 };
53
54 Tower towers[TOWER_MAX_COUNT];
55 int towerCount = 0;
56
57 Model towerModels[TOWER_TYPE_COUNT];
58
59 // definition of our archer unit
60 SpriteUnit archerUnit = {
61 .animations[0] = {
62 .srcRect = {0, 0, 16, 16},
63 .offset = {7, 1},
64 .frameCount = 1,
65 .frameDuration = 0.0f,
66 },
67 .animations[1] = {
68 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
69 .srcRect = {16, 0, 6, 16},
70 .offset = {8, 0},
71 },
72 .animations[2] = {
73 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
74 .srcRect = {22, 0, 11, 16},
75 .offset = {10, 0},
76 },
77 };
78
79 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
80 {
81 float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
82 float xScale = flip ? -1.0f : 1.0f;
83 Camera3D camera = currentLevel->camera;
84 float size = 0.5f * unitScale;
85 // we want the sprite to face the camera, so we need to calculate the up vector
86 Vector3 forward = Vector3Subtract(camera.target, camera.position);
87 Vector3 up = {0, 1, 0};
88 Vector3 right = Vector3CrossProduct(forward, up);
89 up = Vector3Normalize(Vector3CrossProduct(right, forward));
90
91 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
92 {
93 SpriteAnimation anim = unit.animations[i];
94 if (anim.animationId != phase && anim.animationId != 0)
95 {
96 continue;
97 }
98 Rectangle srcRect = anim.srcRect;
99 if (anim.frameCount > 1)
100 {
101 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
102 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
103 }
104 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
105 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
106
107 if (flip)
108 {
109 srcRect.x += srcRect.width;
110 srcRect.width = -srcRect.width;
111 offset.x = scale.x - offset.x;
112 }
113 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
114 // move the sprite slightly towards the camera to avoid z-fighting
115 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
116 }
117 }
118
119 void TowerInit()
120 {
121 for (int i = 0; i < TOWER_MAX_COUNT; i++)
122 {
123 towers[i] = (Tower){0};
124 }
125 towerCount = 0;
126
127 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
128 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
129
130 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
131 {
132 if (towerModels[i].materials)
133 {
134 // assign the palette texture to the material of the model (0 is not used afaik)
135 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
136 }
137 }
138 }
139
140 static void TowerGunUpdate(Tower *tower)
141 {
142 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
143 if (tower->cooldown <= 0.0f)
144 {
145 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
146 if (enemy)
147 {
148 tower->cooldown = config.cooldown;
149 // shoot the enemy; determine future position of the enemy
150 float bulletSpeed = config.projectileSpeed;
151 Vector2 velocity = enemy->simVelocity;
152 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
153 Vector2 towerPosition = {tower->x, tower->y};
154 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
155 for (int i = 0; i < 8; i++) {
156 velocity = enemy->simVelocity;
157 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
158 float distance = Vector2Distance(towerPosition, futurePosition);
159 float eta2 = distance / bulletSpeed;
160 if (fabs(eta - eta2) < 0.01f) {
161 break;
162 }
163 eta = (eta2 + eta) * 0.5f;
164 }
165
166 ProjectileTryAdd(config.projectileType, enemy,
167 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
168 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
169 bulletSpeed, config.hitEffect);
170 enemy->futureDamage += config.hitEffect.damage;
171 tower->lastTargetPosition = futurePosition;
172 }
173 }
174 else
175 {
176 tower->cooldown -= gameTime.deltaTime;
177 }
178 }
179
180 Tower *TowerGetAt(int16_t x, int16_t y)
181 {
182 for (int i = 0; i < towerCount; i++)
183 {
184 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
185 {
186 return &towers[i];
187 }
188 }
189 return 0;
190 }
191
192 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
193 {
194 if (towerCount >= TOWER_MAX_COUNT)
195 {
196 return 0;
197 }
198
199 Tower *tower = TowerGetAt(x, y);
200 if (tower)
201 {
202 return 0;
203 }
204
205 tower = &towers[towerCount++];
206 tower->x = x;
207 tower->y = y;
208 tower->towerType = towerType;
209 tower->cooldown = 0.0f;
210 tower->damage = 0.0f;
211 return tower;
212 }
213
214 Tower *GetTowerByType(uint8_t towerType)
215 {
216 for (int i = 0; i < towerCount; i++)
217 {
218 if (towers[i].towerType == towerType)
219 {
220 return &towers[i];
221 }
222 }
223 return 0;
224 }
225
226 const char *GetTowerName(uint8_t towerType)
227 {
228 return towerTypeConfigs[towerType].name;
229 }
230
231 int GetTowerCosts(uint8_t towerType)
232 {
233 return towerTypeConfigs[towerType].cost;
234 }
235
236 float TowerGetMaxHealth(Tower *tower)
237 {
238 return towerTypeConfigs[tower->towerType].maxHealth;
239 }
240
241 void TowerDrawSingle(Tower tower)
242 {
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 return;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275
276 void TowerDraw()
277 {
278 for (int i = 0; i < towerCount; i++)
279 {
280 TowerDrawSingle(towers[i]);
281 }
282 }
283
284 void TowerUpdate()
285 {
286 for (int i = 0; i < towerCount; i++)
287 {
288 Tower *tower = &towers[i];
289 switch (tower->towerType)
290 {
291 case TOWER_TYPE_CATAPULT:
292 case TOWER_TYPE_BALLISTA:
293 case TOWER_TYPE_ARCHER:
294 TowerGunUpdate(tower);
295 break;
296 }
297 }
298 }
299
300 void TowerDrawHealthBars(Camera3D camera)
301 {
302 for (int i = 0; i < towerCount; i++)
303 {
304 Tower *tower = &towers[i];
305 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
306 {
307 continue;
308 }
309
310 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
311 float maxHealth = TowerGetMaxHealth(tower);
312 float health = maxHealth - tower->damage;
313 float healthRatio = health / maxHealth;
314
315 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
316 }
317 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 8.0f,
32 .speed = 0.5f,
33 .radius = 0.25f,
34 .maxAcceleration = 1.0f,
35 .explosionDamage = 2.0f,
36 .requiredContactTime = 0.5f,
37 .explosionRange = 1.0f,
38 .explosionPushbackPower = 0.25f,
39 .goldValue = 3,
40 .shieldDamageAbsorption = 4.0f,
41 .shieldHealth = 25.0f,
42 },
43 [ENEMY_TYPE_BOSS] = {
44 .health = 50.0f,
45 .speed = 0.4f,
46 .radius = 0.25f,
47 .maxAcceleration = 1.0f,
48 .explosionDamage = 5.0f,
49 .requiredContactTime = 0.5f,
50 .explosionRange = 1.0f,
51 .explosionPushbackPower = 0.25f,
52 .goldValue = 10,
53 },
54 };
55
56 Enemy enemies[ENEMY_MAX_COUNT];
57 int enemyCount = 0;
58
59 SpriteUnit enemySprites[] = {
60 [ENEMY_TYPE_MINION] = {
61 .animations[0] = {
62 .srcRect = {0, 17, 16, 15},
63 .offset = {8.0f, 0.0f},
64 .frameCount = 6,
65 .frameDuration = 0.1f,
66 },
67 .animations[1] = {
68 .srcRect = {1, 33, 15, 14},
69 .offset = {7.0f, 0.0f},
70 .frameCount = 6,
71 .frameWidth = 16,
72 .frameDuration = 0.1f,
73 },
74 },
75 [ENEMY_TYPE_RUNNER] = {
76 .scale = 0.75f,
77 .animations[0] = {
78 .srcRect = {0, 17, 16, 15},
79 .offset = {8.0f, 0.0f},
80 .frameCount = 6,
81 .frameDuration = 0.1f,
82 },
83 },
84 [ENEMY_TYPE_SHIELD] = {
85 .animations[0] = {
86 .srcRect = {0, 17, 16, 15},
87 .offset = {8.0f, 0.0f},
88 .frameCount = 6,
89 .frameDuration = 0.1f,
90 },
91 .animations[1] = {
92 .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
93 .srcRect = {99, 17, 10, 11},
94 .offset = {7.0f, 0.0f},
95 },
96 },
97 [ENEMY_TYPE_BOSS] = {
98 .scale = 1.5f,
99 .animations[0] = {
100 .srcRect = {0, 17, 16, 15},
101 .offset = {8.0f, 0.0f},
102 .frameCount = 6,
103 .frameDuration = 0.1f,
104 },
105 .animations[1] = {
106 .srcRect = {97, 29, 14, 7},
107 .offset = {7.0f, -9.0f},
108 },
109 },
110 };
111
112 void EnemyInit()
113 {
114 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115 {
116 enemies[i] = (Enemy){0};
117 }
118 enemyCount = 0;
119 }
120
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123 return enemyClassConfigs[enemy->enemyType].speed;
124 }
125
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128 return enemyClassConfigs[enemy->enemyType].health;
129 }
130
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133 int16_t castleX = 0;
134 int16_t castleY = 0;
135 int16_t dx = castleX - currentX;
136 int16_t dy = castleY - currentY;
137 if (dx == 0 && dy == 0)
138 {
139 *nextX = currentX;
140 *nextY = currentY;
141 return 1;
142 }
143 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144
145 if (gradient.x == 0 && gradient.y == 0)
146 {
147 *nextX = currentX;
148 *nextY = currentY;
149 return 1;
150 }
151
152 if (fabsf(gradient.x) > fabsf(gradient.y))
153 {
154 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155 *nextY = currentY;
156 return 0;
157 }
158 *nextX = currentX;
159 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160 return 0;
161 }
162
163
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167 const float pointReachedDistance = 0.25f;
168 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169 const float maxSimStepTime = 0.015625f;
170
171 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173 int16_t nextX = enemy->nextX;
174 int16_t nextY = enemy->nextY;
175 Vector2 position = enemy->simPosition;
176 int passedCount = 0;
177 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178 {
179 float stepTime = fminf(deltaT - t, maxSimStepTime);
180 Vector2 target = (Vector2){nextX, nextY};
181 float speed = Vector2Length(*velocity);
182 // draw the target position for debugging
183 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186 {
187 // we reached the target position, let's move to the next waypoint
188 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189 target = (Vector2){nextX, nextY};
190 // track how many waypoints we passed
191 passedCount++;
192 }
193
194 // acceleration towards the target
195 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197 *velocity = Vector2Add(*velocity, acceleration);
198
199 // limit the speed to the maximum speed
200 if (speed > maxSpeed)
201 {
202 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203 }
204
205 // move the enemy
206 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207 }
208
209 if (waypointPassedCount)
210 {
211 (*waypointPassedCount) = passedCount;
212 }
213
214 return position;
215 }
216
217 void EnemyDraw()
218 {
219 rlDrawRenderBatchActive();
220 rlDisableDepthMask();
221 for (int i = 0; i < enemyCount; i++)
222 {
223 Enemy enemy = enemies[i];
224 if (enemy.enemyType == ENEMY_TYPE_NONE)
225 {
226 continue;
227 }
228
229 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230
231 // don't draw any trails for now; might replace this with footprints later
232 // if (enemy.movePathCount > 0)
233 // {
234 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236 // }
237 // for (int j = 1; j < enemy.movePathCount; j++)
238 // {
239 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241 // DrawLine3D(p, q, GREEN);
242 // }
243
244 float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245 int phase = 0;
246 if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247 {
248 phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249 }
250
251 switch (enemy.enemyType)
252 {
253 case ENEMY_TYPE_MINION:
254 case ENEMY_TYPE_RUNNER:
255 case ENEMY_TYPE_SHIELD:
256 case ENEMY_TYPE_BOSS:
257 DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y},
258 enemy.walkedDistance, 0, phase);
259 break;
260 }
261 }
262 rlDrawRenderBatchActive();
263 rlEnableDepthMask();
264 }
265
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268 // damage the tower
269 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272 float explosionRange2 = explosionRange * explosionRange;
273 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274 // explode the enemy
275 if (tower->damage >= TowerGetMaxHealth(tower))
276 {
277 tower->towerType = TOWER_TYPE_NONE;
278 }
279
280 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
281 explosionSource,
282 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283
284 enemy->enemyType = ENEMY_TYPE_NONE;
285
286 // push back enemies & dealing damage
287 for (int i = 0; i < enemyCount; i++)
288 {
289 Enemy *other = &enemies[i];
290 if (other->enemyType == ENEMY_TYPE_NONE)
291 {
292 continue;
293 }
294 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295 if (distanceSqr > 0 && distanceSqr < explosionRange2)
296 {
297 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299 EnemyAddDamage(other, explosionDamge);
300 }
301 }
302 }
303
304 void EnemyUpdate()
305 {
306 const float castleX = 0;
307 const float castleY = 0;
308 const float maxPathDistance2 = 0.25f * 0.25f;
309
310 for (int i = 0; i < enemyCount; i++)
311 {
312 Enemy *enemy = &enemies[i];
313 if (enemy->enemyType == ENEMY_TYPE_NONE)
314 {
315 continue;
316 }
317
318 int waypointPassedCount = 0;
319 Vector2 prevPosition = enemy->simPosition;
320 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321 enemy->startMovingTime = gameTime.time;
322 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323 // track path of unit
324 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325 {
326 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327 {
328 enemy->movePath[j] = enemy->movePath[j - 1];
329 }
330 enemy->movePath[0] = enemy->simPosition;
331 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332 {
333 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334 }
335 }
336
337 if (waypointPassedCount > 0)
338 {
339 enemy->currentX = enemy->nextX;
340 enemy->currentY = enemy->nextY;
341 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343 {
344 // enemy reached the castle; remove it
345 enemy->enemyType = ENEMY_TYPE_NONE;
346 continue;
347 }
348 }
349 }
350
351 // handle collisions between enemies
352 for (int i = 0; i < enemyCount - 1; i++)
353 {
354 Enemy *enemyA = &enemies[i];
355 if (enemyA->enemyType == ENEMY_TYPE_NONE)
356 {
357 continue;
358 }
359 for (int j = i + 1; j < enemyCount; j++)
360 {
361 Enemy *enemyB = &enemies[j];
362 if (enemyB->enemyType == ENEMY_TYPE_NONE)
363 {
364 continue;
365 }
366 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369 float radiusSum = radiusA + radiusB;
370 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371 {
372 // collision
373 float distance = sqrtf(distanceSqr);
374 float overlap = radiusSum - distance;
375 // move the enemies apart, but softly; if we have a clog of enemies,
376 // moving them perfectly apart can cause them to jitter
377 float positionCorrection = overlap / 5.0f;
378 Vector2 direction = (Vector2){
379 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383 }
384 }
385 }
386
387 // handle collisions between enemies and towers
388 for (int i = 0; i < enemyCount; i++)
389 {
390 Enemy *enemy = &enemies[i];
391 if (enemy->enemyType == ENEMY_TYPE_NONE)
392 {
393 continue;
394 }
395 enemy->contactTime -= gameTime.deltaTime;
396 if (enemy->contactTime < 0.0f)
397 {
398 enemy->contactTime = 0.0f;
399 }
400
401 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402 // linear search over towers; could be optimized by using path finding tower map,
403 // but for now, we keep it simple
404 for (int j = 0; j < towerCount; j++)
405 {
406 Tower *tower = &towers[j];
407 if (tower->towerType == TOWER_TYPE_NONE)
408 {
409 continue;
410 }
411 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413 if (distanceSqr > combinedRadius * combinedRadius)
414 {
415 continue;
416 }
417 // potential collision; square / circle intersection
418 float dx = tower->x - enemy->simPosition.x;
419 float dy = tower->y - enemy->simPosition.y;
420 float absDx = fabsf(dx);
421 float absDy = fabsf(dy);
422 Vector3 contactPoint = {0};
423 if (absDx <= 0.5f && absDx <= absDy) {
424 // vertical collision; push the enemy out horizontally
425 float overlap = enemyRadius + 0.5f - absDy;
426 if (overlap < 0.0f)
427 {
428 continue;
429 }
430 float direction = dy > 0.0f ? -1.0f : 1.0f;
431 enemy->simPosition.y += direction * overlap;
432 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433 }
434 else if (absDy <= 0.5f && absDy <= absDx)
435 {
436 // horizontal collision; push the enemy out vertically
437 float overlap = enemyRadius + 0.5f - absDx;
438 if (overlap < 0.0f)
439 {
440 continue;
441 }
442 float direction = dx > 0.0f ? -1.0f : 1.0f;
443 enemy->simPosition.x += direction * overlap;
444 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445 }
446 else
447 {
448 // possible collision with a corner
449 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451 float cornerX = tower->x + cornerDX;
452 float cornerY = tower->y + cornerDY;
453 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454 if (cornerDistanceSqr > enemyRadius * enemyRadius)
455 {
456 continue;
457 }
458 // push the enemy out along the diagonal
459 float cornerDistance = sqrtf(cornerDistanceSqr);
460 float overlap = enemyRadius - cornerDistance;
461 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463 enemy->simPosition.x -= directionX * overlap;
464 enemy->simPosition.y -= directionY * overlap;
465 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466 }
467
468 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469 {
470 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472 {
473 EnemyTriggerExplode(enemy, tower, contactPoint);
474 }
475 }
476 }
477 }
478 }
479
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482 return (EnemyId){enemy - enemies, enemy->generation};
483 }
484
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487 if (enemyId.index >= ENEMY_MAX_COUNT)
488 {
489 return 0;
490 }
491 Enemy *enemy = &enemies[enemyId.index];
492 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493 {
494 return 0;
495 }
496 return enemy;
497 }
498
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501 Enemy *spawn = 0;
502 for (int i = 0; i < enemyCount; i++)
503 {
504 Enemy *enemy = &enemies[i];
505 if (enemy->enemyType == ENEMY_TYPE_NONE)
506 {
507 spawn = enemy;
508 break;
509 }
510 }
511
512 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513 {
514 spawn = &enemies[enemyCount++];
515 }
516
517 if (spawn)
518 {
519 *spawn = (Enemy){
520 .currentX = currentX,
521 .currentY = currentY,
522 .nextX = currentX,
523 .nextY = currentY,
524 .simPosition = (Vector2){currentX, currentY},
525 .simVelocity = (Vector2){0, 0},
526 .enemyType = enemyType,
527 .startMovingTime = gameTime.time,
528 .movePathCount = 0,
529 .walkedDistance = 0.0f,
530 .shieldDamage = 0.0f,
531 .damage = 0.0f,
532 .futureDamage = 0.0f,
533 .contactTime = 0.0f,
534 .generation = spawn->generation + 1,
535 };
536 }
537
538 return spawn;
539 }
540
541 int EnemyAddDamageRange(Vector2 position, float range, float damage)
542 {
543 int count = 0;
544 float range2 = range * range;
545 for (int i = 0; i < enemyCount; i++)
546 {
547 Enemy *enemy = &enemies[i];
548 if (enemy->enemyType == ENEMY_TYPE_NONE)
549 {
550 continue;
551 }
552 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
553 if (distance2 <= range2)
554 {
555 EnemyAddDamage(enemy, damage);
556 count++;
557 }
558 }
559 return count;
560 }
561
562 int EnemyAddDamage(Enemy *enemy, float damage)
563 {
564 float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
565 if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
566 {
567 float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
568 float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
569 enemy->shieldDamage += shieldDamage;
570 damage -= shieldDamage;
571 }
572 enemy->damage += damage;
573 if (enemy->damage >= EnemyGetMaxHealth(enemy))
574 {
575 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
576 enemy->enemyType = ENEMY_TYPE_NONE;
577 return 1;
578 }
579
580 return 0;
581 }
582
583 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
584 {
585 int16_t castleX = 0;
586 int16_t castleY = 0;
587 Enemy* closest = 0;
588 int16_t closestDistance = 0;
589 float range2 = range * range;
590 for (int i = 0; i < enemyCount; i++)
591 {
592 Enemy* enemy = &enemies[i];
593 if (enemy->enemyType == ENEMY_TYPE_NONE)
594 {
595 continue;
596 }
597 float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
598 if (enemy->futureDamage >= maxHealth)
599 {
600 // ignore enemies that will die soon
601 continue;
602 }
603 int16_t dx = castleX - enemy->currentX;
604 int16_t dy = castleY - enemy->currentY;
605 int16_t distance = abs(dx) + abs(dy);
606 if (!closest || distance < closestDistance)
607 {
608 float tdx = towerX - enemy->currentX;
609 float tdy = towerY - enemy->currentY;
610 float tdistance2 = tdx * tdx + tdy * tdy;
611 if (tdistance2 <= range2)
612 {
613 closest = enemy;
614 closestDistance = distance;
615 }
616 }
617 }
618 return closest;
619 }
620
621 int EnemyCount()
622 {
623 int count = 0;
624 for (int i = 0; i < enemyCount; i++)
625 {
626 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
627 {
628 count++;
629 }
630 }
631 return count;
632 }
633
634 void EnemyDrawHealthbars(Camera3D camera)
635 {
636 for (int i = 0; i < enemyCount; i++)
637 {
638 Enemy *enemy = &enemies[i];
639
640 float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
641 if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
642 {
643 float shieldHealth = maxShieldHealth - enemy->shieldDamage;
644 float shieldHealthRatio = shieldHealth / maxShieldHealth;
645 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
646 DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
647 }
648
649 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
650 {
651 continue;
652 }
653 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
654 float maxHealth = EnemyGetMaxHealth(enemy);
655 float health = maxHealth - enemy->damage;
656 float healthRatio = health / maxHealth;
657
658 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
659 }
660 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
This involved a couple of changes here and there, but nothing too big in general.
The first update was the sprite atlas to include some UI elements:
On the right, there is a panel that can be used for panel UI elements and furthermore there are 4 button variants that represent the different states of a button:
- Normal
- Disabled
- Hovered
- Pressed
The pressed state is only active for a single frame, so it does not fit in so well in the current way how buttons work here, but it might be useful in the future.
In order to scale the buttons and the panel, we use the 9-slice scaling technique. In raylib, this can be configured via the NPatchInfo struct and drawn with DrawTextureNPatch. When drawing a texture with 9-slice scaling, the texture is divided into 9 parts: 4 corners, 4 edges and 1 center. The corners are drawn as they are, the edges are stretched and the center is stretched in both directions. It's a simple way to create scalable UI elements that has been around for a long time.
Here is a raylib example that demonstrates the 9-slice scaling feature.While it could be fun to continue working out the visual design of the UI, this is still too early and there are many more important things we want to implement more sooner than later.
Conclusion
We have now a generic context menu that can be utilized for different purposes. The tower selection for building new towers is now cleaner. Using the context menu system for a confirmation dialog works well. The next step is to implement the tower upgrade mechanic and its UI. This will be the topic of the next part.