Simple tower defense tutorial, part 18: Tower upgrading, 2/2
Since we have now the means to draw customizable context menus, we can focus on implementing the tower upgrading feature. There are two parts: Working out the UI and implementing the game logic. Let's start with the UI since we need it to test the game logic part.
UI
The plan:
- There are 3 upgrade categories: Damage, Range, and Rate of fire.
- With each upgrade level, the tower gains a stage.
- A tower has up to 10 stages.
- Each stage increases the costs for the next upgrade level.
The player can therefore upgrade a single category up to 10 times while the other two categories would have to remain at level 0.
There is no strong reason for this design - I have no idea if this is a good idea or not. But I believe there should be some kind of upgrade system and this is something quite simple to implement. A more sophisticated and potentially cooler system would be an upgrade tree, but that is much more complex to implement. So let's start simple and see where it takes us!
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 CONTEXT_MENU_TYPE_UPGRADE,
649 };
650
651 enum UpgradeType
652 {
653 UPGRADE_TYPE_SPEED,
654 UPGRADE_TYPE_DAMAGE,
655 UPGRADE_TYPE_RANGE,
656 };
657
658 typedef struct ContextMenuArgs
659 {
660 void *data;
661 uint8_t uint8;
662 int32_t int32;
663 Tower *tower;
664 } ContextMenuArgs;
665
666 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
667 {
668 uint8_t towerType = data->uint8;
669 level->placementMode = towerType;
670 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
671 return 1;
672 }
673
674 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
675 {
676 Tower *tower = data->tower;
677 int gold = data->int32;
678 level->playerGold += gold;
679 tower->towerType = TOWER_TYPE_NONE;
680 return 1;
681 }
682
683 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
684 {
685 return 1;
686 }
687
688 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
689 {
690 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
691 return 0;
692 }
693
694 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
695 {
696 Tower *tower = data->tower;
697 switch (data->uint8)
698 {
699 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
700 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
701 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
702 }
703 level->playerGold -= data->int32;
704 return 0;
705 }
706
707 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
708 {
709 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
710 return 0;
711 }
712
713 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
714 {
715 Tower *tower = data->tower;
716 if (level->playerGold >= 1)
717 {
718 level->playerGold -= 1;
719 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
720 }
721 return tower->damage == 0.0f;
722 }
723
724 typedef struct ContextMenuItem
725 {
726 uint8_t index;
727 char text[24];
728 float alignX;
729 int (*action)(Level*, ContextMenuArgs*);
730 void *data;
731 ContextMenuArgs args;
732 ButtonState buttonState;
733 } ContextMenuItem;
734
735 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
736 {
737 ContextMenuItem item = {.index = index, .alignX = alignX};
738 strncpy(item.text, text, 24);
739 return item;
740 }
741
742 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
743 {
744 ContextMenuItem item = {.index = index, .action = action, .args = args};
745 strncpy(item.text, text, 24);
746 return item;
747 }
748
749 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
750 {
751 const int itemHeight = 28;
752 const int itemSpacing = 1;
753 const int padding = 8;
754 int itemCount = 0;
755 for (int i = 0; menus[i].text[0]; i++)
756 {
757 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
758 }
759
760 Rectangle contextMenu = {0, 0, width,
761 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
762
763 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
764 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
765
766 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
767 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
768 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
769 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
770
771 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
772 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
773 const int itemX = contextMenu.x + itemSpacing;
774 const int itemWidth = contextMenu.width - itemSpacing * 2;
775 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
776 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
777 int status = 0;
778 for (int i = 0; menus[i].text[0]; i++)
779 {
780 if (menus[i].action)
781 {
782 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
783 {
784 status = menus[i].action(level, &menus[i].args);
785 }
786 }
787 else
788 {
789 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
790 }
791 }
792
793 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
794 {
795 return 1;
796 }
797
798 return status;
799 }
800
801 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
802 {
803 ContextMenuItem menu[12] = {0};
804 int menuCount = 0;
805 int menuIndex = 0;
806 if (tower)
807 {
808
809 if (tower) {
810 menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f);
811 }
812
813 // two texts, same line
814 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
815 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
816
817 if (tower->towerType != TOWER_TYPE_BASE)
818 {
819 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
820 (ContextMenuArgs){.tower = tower});
821 }
822
823 if (tower->towerType != TOWER_TYPE_BASE)
824 {
825
826 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
827 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
828 }
829 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
830 {
831 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
832 (ContextMenuArgs){.tower = tower});
833 }
834 }
835 else
836 {
837 menu[menuCount] = ContextMenuItemButton(menuIndex++,
838 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)),
839 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
840 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL);
841
842 menu[menuCount] = ContextMenuItemButton(menuIndex++,
843 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)),
844 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
845 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER);
846
847 menu[menuCount] = ContextMenuItemButton(menuIndex++,
848 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)),
849 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
850 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA);
851
852 menu[menuCount] = ContextMenuItemButton(menuIndex++,
853 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)),
854 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
855 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT);
856 }
857
858 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
859 {
860 level->placementContextMenuStatus = -1;
861 }
862 }
863
864 void DrawLevelBuildingState(Level *level)
865 {
866 BeginMode3D(level->camera);
867 DrawLevelGround(level);
868
869 PathFindingMapUpdate(0, 0);
870 TowerDraw();
871 EnemyDraw();
872 ProjectileDraw();
873 ParticleDraw();
874 DrawEnemyPaths(level);
875
876 guiState.isBlocked = 0;
877
878 // when the context menu is not active, we update the placement position
879 if (level->placementContextMenuStatus == 0)
880 {
881 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
882 float hitDistance = ray.position.y / -ray.direction.y;
883 float hitX = ray.direction.x * hitDistance + ray.position.x;
884 float hitY = ray.direction.z * hitDistance + ray.position.z;
885 level->placementX = (int)floorf(hitX + 0.5f);
886 level->placementY = (int)floorf(hitY + 0.5f);
887 }
888
889 // Hover rectangle, when the mouse is over the map
890 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
891 if (isHovering)
892 {
893 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, RED);
894 }
895
896 EndMode3D();
897
898 TowerDrawHealthBars(level->camera);
899
900 DrawTitle("Building phase");
901
902 // Draw the context menu when the context menu is active
903 if (level->placementContextMenuStatus >= 1)
904 {
905 Tower *tower = TowerGetAt(level->placementX, level->placementY);
906 float maxHitpoints = 0.0f;
907 float hp = 0.0f;
908 float damageFactor = 0.0f;
909 int32_t sellValue = 0;
910
911 if (tower)
912 {
913 maxHitpoints = TowerGetMaxHealth(tower);
914 hp = maxHitpoints - tower->damage;
915 damageFactor = 1.0f - tower->damage / maxHitpoints;
916 sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
917 }
918
919 ContextMenuItem menu[12] = {0};
920 int menuCount = 0;
921 int menuIndex = 0;
922 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
923 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
924
925 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
926 {
927 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
928 }
929 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
930 {
931 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
932 int costs = totalLevel * 4;
933 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
934 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
935 GetTowerName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
936 int buttonMenuIndex = menuIndex;
937 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
938 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
939 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
940 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
941 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
942 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
943
944 // check if buttons should be disabled
945 if (isMaxLevel || level->playerGold < costs)
946 {
947 for (int i = buttonMenuIndex; i < menuCount; i++)
948 {
949 menu[i].buttonState.isDisabled = 1;
950 }
951 }
952
953 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
954 {
955 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
956 }
957 }
958 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
959 {
960 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f);
961 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
962 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
963 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
964 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
965 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
966 {
967 level->placementContextMenuStatus = -1;
968 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
969 }
970 }
971 }
972
973 // Activate the context menu when the mouse is clicked and the context menu is not active
974 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
975 {
976 level->placementContextMenuStatus += 1;
977 }
978
979 if (level->placementContextMenuStatus == 0)
980 {
981 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
982 {
983 level->nextState = LEVEL_STATE_RESET;
984 }
985
986 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
987 {
988 level->nextState = LEVEL_STATE_BATTLE;
989 }
990
991 }
992 }
993
994 void InitBattleStateConditions(Level *level)
995 {
996 level->state = LEVEL_STATE_BATTLE;
997 level->nextState = LEVEL_STATE_NONE;
998 level->waveEndTimer = 0.0f;
999 for (int i = 0; i < 10; i++)
1000 {
1001 EnemyWave *wave = &level->waves[i];
1002 wave->spawned = 0;
1003 wave->timeToSpawnNext = wave->delay;
1004 }
1005 }
1006
1007 void DrawLevelBattleState(Level *level)
1008 {
1009 BeginMode3D(level->camera);
1010 DrawLevelGround(level);
1011 TowerDraw();
1012 EnemyDraw();
1013 ProjectileDraw();
1014 ParticleDraw();
1015 guiState.isBlocked = 0;
1016 EndMode3D();
1017
1018 EnemyDrawHealthbars(level->camera);
1019 TowerDrawHealthBars(level->camera);
1020
1021 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1022 {
1023 level->nextState = LEVEL_STATE_RESET;
1024 }
1025
1026 int maxCount = 0;
1027 int remainingCount = 0;
1028 for (int i = 0; i < 10; i++)
1029 {
1030 EnemyWave *wave = &level->waves[i];
1031 if (wave->wave != level->currentWave)
1032 {
1033 continue;
1034 }
1035 maxCount += wave->count;
1036 remainingCount += wave->count - wave->spawned;
1037 }
1038 int aliveCount = EnemyCount();
1039 remainingCount += aliveCount;
1040
1041 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1042 DrawTitle(text);
1043 }
1044
1045 void DrawLevel(Level *level)
1046 {
1047 switch (level->state)
1048 {
1049 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1050 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1051 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1052 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1053 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1054 default: break;
1055 }
1056
1057 DrawLevelHud(level);
1058 }
1059
1060 void UpdateLevel(Level *level)
1061 {
1062 if (level->state == LEVEL_STATE_BATTLE)
1063 {
1064 int activeWaves = 0;
1065 for (int i = 0; i < 10; i++)
1066 {
1067 EnemyWave *wave = &level->waves[i];
1068 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1069 {
1070 continue;
1071 }
1072 activeWaves++;
1073 wave->timeToSpawnNext -= gameTime.deltaTime;
1074 if (wave->timeToSpawnNext <= 0.0f)
1075 {
1076 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1077 if (enemy)
1078 {
1079 wave->timeToSpawnNext = wave->interval;
1080 wave->spawned++;
1081 }
1082 }
1083 }
1084 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1085 level->waveEndTimer += gameTime.deltaTime;
1086 if (level->waveEndTimer >= 2.0f)
1087 {
1088 level->nextState = LEVEL_STATE_LOST_WAVE;
1089 }
1090 }
1091 else if (activeWaves == 0 && EnemyCount() == 0)
1092 {
1093 level->waveEndTimer += gameTime.deltaTime;
1094 if (level->waveEndTimer >= 2.0f)
1095 {
1096 level->nextState = LEVEL_STATE_WON_WAVE;
1097 }
1098 }
1099 }
1100
1101 PathFindingMapUpdate(0, 0);
1102 EnemyUpdate();
1103 TowerUpdate();
1104 ProjectileUpdate();
1105 ParticleUpdate();
1106
1107 if (level->nextState == LEVEL_STATE_RESET)
1108 {
1109 InitLevel(level);
1110 }
1111
1112 if (level->nextState == LEVEL_STATE_BATTLE)
1113 {
1114 InitBattleStateConditions(level);
1115 }
1116
1117 if (level->nextState == LEVEL_STATE_WON_WAVE)
1118 {
1119 level->currentWave++;
1120 level->state = LEVEL_STATE_WON_WAVE;
1121 }
1122
1123 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1124 {
1125 level->state = LEVEL_STATE_LOST_WAVE;
1126 }
1127
1128 if (level->nextState == LEVEL_STATE_BUILDING)
1129 {
1130 level->state = LEVEL_STATE_BUILDING;
1131 level->placementContextMenuStatus = 0;
1132 }
1133
1134 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1135 {
1136 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1137 level->placementTransitionPosition = (Vector2){
1138 level->placementX, level->placementY};
1139 // initialize the spring to the current position
1140 level->placementTowerSpring = (PhysicsPoint){
1141 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1142 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1143 };
1144 level->placementPhase = PLACEMENT_PHASE_STARTING;
1145 level->placementTimer = 0.0f;
1146 }
1147
1148 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1149 {
1150 // make something of this later
1151 InitLevel(level);
1152 }
1153
1154 level->nextState = LEVEL_STATE_NONE;
1155 }
1156
1157 float nextSpawnTime = 0.0f;
1158
1159 void ResetGame()
1160 {
1161 InitLevel(currentLevel);
1162 }
1163
1164 void InitGame()
1165 {
1166 TowerInit();
1167 EnemyInit();
1168 ProjectileInit();
1169 ParticleInit();
1170 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1171
1172 currentLevel = levels;
1173 InitLevel(currentLevel);
1174 }
1175
1176 //# Immediate GUI functions
1177
1178 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1179 {
1180 const float healthBarHeight = 6.0f;
1181 const float healthBarOffset = 15.0f;
1182 const float inset = 2.0f;
1183 const float innerWidth = healthBarWidth - inset * 2;
1184 const float innerHeight = healthBarHeight - inset * 2;
1185
1186 Vector2 screenPos = GetWorldToScreen(position, camera);
1187 screenPos = Vector2Add(screenPos, screenOffset);
1188 float centerX = screenPos.x - healthBarWidth * 0.5f;
1189 float topY = screenPos.y - healthBarOffset;
1190 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1191 float healthWidth = innerWidth * healthRatio;
1192 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1193 }
1194
1195 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1196 {
1197 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1198
1199 DrawTextEx(gameFontNormal, text, (Vector2){
1200 x + (width - textSize.x) * alignX,
1201 y + (height - textSize.y) * alignY
1202 }, gameFontNormal.baseSize, 1, textColor);
1203 }
1204
1205 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1206 {
1207 Rectangle bounds = {x, y, width, height};
1208 int isPressed = 0;
1209 int isSelected = state && state->isSelected;
1210 int isDisabled = state && state->isDisabled;
1211 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1212 {
1213 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1214 {
1215 isPressed = 1;
1216 }
1217 guiState.isBlocked = 1;
1218 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1219 bounds, Vector2Zero(), 0, WHITE);
1220 }
1221 else
1222 {
1223 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1224 bounds, Vector2Zero(), 0, WHITE);
1225 }
1226 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1227 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1228 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1229 return isPressed;
1230 }
1231
1232 //# Main game loop
1233
1234 void GameUpdate()
1235 {
1236 UpdateLevel(currentLevel);
1237 }
1238
1239 int main(void)
1240 {
1241 int screenWidth, screenHeight;
1242 GetPreferredSize(&screenWidth, &screenHeight);
1243 InitWindow(screenWidth, screenHeight, "Tower defense");
1244 float gamespeed = 1.0f;
1245 SetTargetFPS(30);
1246
1247 LoadAssets();
1248 InitGame();
1249
1250 float pause = 1.0f;
1251
1252 while (!WindowShouldClose())
1253 {
1254 if (IsPaused()) {
1255 // canvas is not visible in browser - do nothing
1256 continue;
1257 }
1258
1259 if (IsKeyPressed(KEY_T))
1260 {
1261 gamespeed += 0.1f;
1262 if (gamespeed > 1.05f) gamespeed = 0.1f;
1263 }
1264
1265 if (IsKeyPressed(KEY_P))
1266 {
1267 pause = pause > 0.5f ? 0.0f : 1.0f;
1268 }
1269
1270 float dt = GetFrameTime() * gamespeed * pause;
1271 // cap maximum delta time to 0.1 seconds to prevent large time steps
1272 if (dt > 0.1f) dt = 0.1f;
1273 gameTime.time += dt;
1274 gameTime.deltaTime = dt;
1275 gameTime.frameCount += 1;
1276
1277 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1278 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1279
1280 BeginDrawing();
1281 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1282
1283 GameUpdate();
1284 DrawLevel(currentLevel);
1285
1286 if (gamespeed != 1.0f)
1287 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1288 EndDrawing();
1289
1290 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1291 }
1292
1293 CloseWindow();
1294
1295 return 0;
1296 }
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 #define TOWER_MAX_STAGE 10
74
75 typedef struct TowerUpgradeState
76 {
77 uint8_t range;
78 uint8_t damage;
79 uint8_t speed;
80 } TowerUpgradeState;
81
82 typedef struct Tower
83 {
84 int16_t x, y;
85 uint8_t towerType;
86 TowerUpgradeState upgradeState;
87 Vector2 lastTargetPosition;
88 float cooldown;
89 float damage;
90 } Tower;
91
92 typedef struct GameTime
93 {
94 float time;
95 float deltaTime;
96 uint32_t frameCount;
97
98 float fixedDeltaTime;
99 // leaving the fixed time stepping to the update functions,
100 // we need to know the fixed time at the start of the frame
101 float fixedTimeStart;
102 // and the number of fixed steps that we have to make this frame
103 // The fixedTime is fixedTimeStart + n * fixedStepCount
104 uint8_t fixedStepCount;
105 } GameTime;
106
107 typedef struct ButtonState {
108 char isSelected;
109 char isDisabled;
110 } ButtonState;
111
112 typedef struct GUIState {
113 int isBlocked;
114 } GUIState;
115
116 typedef enum LevelState
117 {
118 LEVEL_STATE_NONE,
119 LEVEL_STATE_BUILDING,
120 LEVEL_STATE_BUILDING_PLACEMENT,
121 LEVEL_STATE_BATTLE,
122 LEVEL_STATE_WON_WAVE,
123 LEVEL_STATE_LOST_WAVE,
124 LEVEL_STATE_WON_LEVEL,
125 LEVEL_STATE_RESET,
126 } LevelState;
127
128 typedef struct EnemyWave {
129 uint8_t enemyType;
130 uint8_t wave;
131 uint16_t count;
132 float interval;
133 float delay;
134 Vector2 spawnPosition;
135
136 uint16_t spawned;
137 float timeToSpawnNext;
138 } EnemyWave;
139
140 #define ENEMY_MAX_WAVE_COUNT 10
141
142 typedef enum PlacementPhase
143 {
144 PLACEMENT_PHASE_STARTING,
145 PLACEMENT_PHASE_MOVING,
146 PLACEMENT_PHASE_PLACING,
147 } PlacementPhase;
148
149 typedef struct Level
150 {
151 int seed;
152 LevelState state;
153 LevelState nextState;
154 Camera3D camera;
155 int placementMode;
156 PlacementPhase placementPhase;
157 float placementTimer;
158
159 int16_t placementX;
160 int16_t placementY;
161 int8_t placementContextMenuStatus;
162 int8_t placementContextMenuType;
163
164 Vector2 placementTransitionPosition;
165 PhysicsPoint placementTowerSpring;
166
167 int initialGold;
168 int playerGold;
169
170 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
171 int currentWave;
172 float waveEndTimer;
173 } Level;
174
175 typedef struct DeltaSrc
176 {
177 char x, y;
178 } DeltaSrc;
179
180 typedef struct PathfindingMap
181 {
182 int width, height;
183 float scale;
184 float *distances;
185 long *towerIndex;
186 DeltaSrc *deltaSrc;
187 float maxDistance;
188 Matrix toMapSpace;
189 Matrix toWorldSpace;
190 } PathfindingMap;
191
192 // when we execute the pathfinding algorithm, we need to store the active nodes
193 // in a queue. Each node has a position, a distance from the start, and the
194 // position of the node that we came from.
195 typedef struct PathfindingNode
196 {
197 int16_t x, y, fromX, fromY;
198 float distance;
199 } PathfindingNode;
200
201 typedef struct EnemyId
202 {
203 uint16_t index;
204 uint16_t generation;
205 } EnemyId;
206
207 typedef struct EnemyClassConfig
208 {
209 float speed;
210 float health;
211 float shieldHealth;
212 float shieldDamageAbsorption;
213 float radius;
214 float maxAcceleration;
215 float requiredContactTime;
216 float explosionDamage;
217 float explosionRange;
218 float explosionPushbackPower;
219 int goldValue;
220 } EnemyClassConfig;
221
222 typedef struct Enemy
223 {
224 int16_t currentX, currentY;
225 int16_t nextX, nextY;
226 Vector2 simPosition;
227 Vector2 simVelocity;
228 uint16_t generation;
229 float walkedDistance;
230 float startMovingTime;
231 float damage, futureDamage;
232 float shieldDamage;
233 float contactTime;
234 uint8_t enemyType;
235 uint8_t movePathCount;
236 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
237 } Enemy;
238
239 // a unit that uses sprites to be drawn
240 #define SPRITE_UNIT_ANIMATION_COUNT 6
241 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
242 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
243 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
244
245 typedef struct SpriteAnimation
246 {
247 Rectangle srcRect;
248 Vector2 offset;
249 uint8_t animationId;
250 uint8_t frameCount;
251 uint8_t frameWidth;
252 float frameDuration;
253 } SpriteAnimation;
254
255 typedef struct SpriteUnit
256 {
257 float scale;
258 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
259 } SpriteUnit;
260
261 #define PROJECTILE_MAX_COUNT 1200
262 #define PROJECTILE_TYPE_NONE 0
263 #define PROJECTILE_TYPE_ARROW 1
264 #define PROJECTILE_TYPE_CATAPULT 2
265 #define PROJECTILE_TYPE_BALLISTA 3
266
267 typedef struct Projectile
268 {
269 uint8_t projectileType;
270 float shootTime;
271 float arrivalTime;
272 float distance;
273 Vector3 position;
274 Vector3 target;
275 Vector3 directionNormal;
276 EnemyId targetEnemy;
277 HitEffectConfig hitEffectConfig;
278 } Projectile;
279
280 //# Function declarations
281 float TowerGetMaxHealth(Tower *tower);
282 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
283 int EnemyAddDamageRange(Vector2 position, float range, float damage);
284 int EnemyAddDamage(Enemy *enemy, float damage);
285
286 //# Enemy functions
287 void EnemyInit();
288 void EnemyDraw();
289 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
290 void EnemyUpdate();
291 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
292 float EnemyGetMaxHealth(Enemy *enemy);
293 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
294 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
295 EnemyId EnemyGetId(Enemy *enemy);
296 Enemy *EnemyTryResolve(EnemyId enemyId);
297 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
298 int EnemyAddDamage(Enemy *enemy, float damage);
299 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
300 int EnemyCount();
301 void EnemyDrawHealthbars(Camera3D camera);
302
303 //# Tower functions
304 void TowerInit();
305 Tower *TowerGetAt(int16_t x, int16_t y);
306 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
307 Tower *GetTowerByType(uint8_t towerType);
308 int GetTowerCosts(uint8_t towerType);
309 const char *GetTowerName(uint8_t towerType);
310 float TowerGetMaxHealth(Tower *tower);
311 void TowerDraw();
312 void TowerDrawSingle(Tower tower);
313 void TowerUpdate();
314 void TowerDrawHealthBars(Camera3D camera);
315 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
316
317 //# Particles
318 void ParticleInit();
319 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
320 void ParticleUpdate();
321 void ParticleDraw();
322
323 //# Projectiles
324 void ProjectileInit();
325 void ProjectileDraw();
326 void ProjectileUpdate();
327 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
328
329 //# Pathfinding map
330 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
331 float PathFindingGetDistance(int mapX, int mapY);
332 Vector2 PathFindingGetGradient(Vector3 world);
333 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
334 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
335 void PathFindingMapDraw();
336
337 //# UI
338 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
339
340 //# Level
341 void DrawLevelGround(Level *level);
342 void DrawEnemyPath(Level *level, Color arrowColor);
343
344 //# variables
345 extern Level *currentLevel;
346 extern Enemy enemies[ENEMY_MAX_COUNT];
347 extern int enemyCount;
348 extern EnemyClassConfig enemyClassConfigs[];
349
350 extern GUIState guiState;
351 extern GameTime gameTime;
352 extern Tower towers[TOWER_MAX_COUNT];
353 extern int towerCount;
354
355 extern Texture2D palette, spriteSheet;
356
357 #endif
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 = (Tower){
207 .x = x,
208 .y = y,
209 .towerType = towerType,
210 .cooldown = 0.0f,
211 .damage = 0.0f,
212 };
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 const char *GetTowerName(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].name;
231 }
232
233 int GetTowerCosts(uint8_t towerType)
234 {
235 return towerTypeConfigs[towerType].cost;
236 }
237
238 float TowerGetMaxHealth(Tower *tower)
239 {
240 return towerTypeConfigs[tower->towerType].maxHealth;
241 }
242
243 void TowerDrawSingle(Tower tower)
244 {
245 if (tower.towerType == TOWER_TYPE_NONE)
246 {
247 return;
248 }
249
250 switch (tower.towerType)
251 {
252 case TOWER_TYPE_ARCHER:
253 {
254 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
255 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
256 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
257 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
258 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
259 }
260 break;
261 case TOWER_TYPE_BALLISTA:
262 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
263 break;
264 case TOWER_TYPE_CATAPULT:
265 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
266 break;
267 default:
268 if (towerModels[tower.towerType].materials)
269 {
270 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
271 } else {
272 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
273 }
274 break;
275 }
276 }
277
278 void TowerDraw()
279 {
280 for (int i = 0; i < towerCount; i++)
281 {
282 TowerDrawSingle(towers[i]);
283 }
284 }
285
286 void TowerUpdate()
287 {
288 for (int i = 0; i < towerCount; i++)
289 {
290 Tower *tower = &towers[i];
291 switch (tower->towerType)
292 {
293 case TOWER_TYPE_CATAPULT:
294 case TOWER_TYPE_BALLISTA:
295 case TOWER_TYPE_ARCHER:
296 TowerGunUpdate(tower);
297 break;
298 }
299 }
300 }
301
302 void TowerDrawHealthBars(Camera3D camera)
303 {
304 for (int i = 0; i < towerCount; i++)
305 {
306 Tower *tower = &towers[i];
307 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
308 {
309 continue;
310 }
311
312 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
313 float maxHealth = TowerGetMaxHealth(tower);
314 float health = maxHealth - tower->damage;
315 float healthRatio = health / maxHealth;
316
317 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
318 }
319 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 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 upgrade status is now stored in the TowerUpgradeState struct (which I added in the previous part already before realizing that I first need some more UI bits).
When placing the tower and when opening the tower's context menu, it would be nice to see the range of the tower. This way we can also verify that the range upgrade works as expected.
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 // calculate x and z rotation to align the model with the spring
575 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
576 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
577 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
578 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
579 float springLength = Vector3Length(towerUp);
580 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
581 float towerSquash = 1.0f / towerStretch;
582
583 Tower dummy = {
584 .towerType = level->placementMode,
585 };
586
587 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
588 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
589 {
590 rangeAlpha = 1.0f - rangeAlpha;
591 }
592 else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
593 {
594 rangeAlpha = 1.0f;
595 }
596
597 TowerDrawRange(dummy, rangeAlpha);
598
599 rlPushMatrix();
600 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
601
602 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
603 rlScalef(towerSquash, towerStretch, towerSquash);
604 TowerDrawSingle(dummy);
605 rlPopMatrix();
606
607
608 // draw a shadow for the tower
609 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
610 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
611 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
612
613
614 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
615 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
616 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
617 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
618
619 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
620 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
621 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
622 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
623 rlPopMatrix();
624
625 guiState.isBlocked = 0;
626
627 EndMode3D();
628
629 TowerDrawHealthBars(level->camera);
630
631 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
632 {
633 if (level->placementTimer > placementDuration)
634 {
635 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
636 // testing repairing
637 tower->damage = 2.5f;
638 level->playerGold -= GetTowerCosts(level->placementMode);
639 level->nextState = LEVEL_STATE_BUILDING;
640 level->placementMode = TOWER_TYPE_NONE;
641 }
642 }
643 else
644 {
645 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
646 {
647 level->nextState = LEVEL_STATE_BUILDING;
648 level->placementMode = TOWER_TYPE_NONE;
649 TraceLog(LOG_INFO, "Cancel building");
650 }
651
652 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
653 {
654 level->placementPhase = PLACEMENT_PHASE_PLACING;
655 level->placementTimer = 0.0f;
656 }
657 }
658 }
659
660 enum ContextMenuType
661 {
662 CONTEXT_MENU_TYPE_MAIN,
663 CONTEXT_MENU_TYPE_SELL_CONFIRM,
664 CONTEXT_MENU_TYPE_UPGRADE,
665 };
666
667 enum UpgradeType
668 {
669 UPGRADE_TYPE_SPEED,
670 UPGRADE_TYPE_DAMAGE,
671 UPGRADE_TYPE_RANGE,
672 };
673
674 typedef struct ContextMenuArgs
675 {
676 void *data;
677 uint8_t uint8;
678 int32_t int32;
679 Tower *tower;
680 } ContextMenuArgs;
681
682 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
683 {
684 uint8_t towerType = data->uint8;
685 level->placementMode = towerType;
686 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
687 return 1;
688 }
689
690 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
691 {
692 Tower *tower = data->tower;
693 int gold = data->int32;
694 level->playerGold += gold;
695 tower->towerType = TOWER_TYPE_NONE;
696 return 1;
697 }
698
699 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
700 {
701 return 1;
702 }
703
704 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
705 {
706 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
707 return 0;
708 }
709
710 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
711 {
712 Tower *tower = data->tower;
713 switch (data->uint8)
714 {
715 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
716 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
717 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
718 }
719 level->playerGold -= data->int32;
720 return 0;
721 }
722
723 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
724 {
725 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
726 return 0;
727 }
728
729 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
730 {
731 Tower *tower = data->tower;
732 if (level->playerGold >= 1)
733 {
734 level->playerGold -= 1;
735 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
736 }
737 return tower->damage == 0.0f;
738 }
739
740 typedef struct ContextMenuItem
741 {
742 uint8_t index;
743 char text[24];
744 float alignX;
745 int (*action)(Level*, ContextMenuArgs*);
746 void *data;
747 ContextMenuArgs args;
748 ButtonState buttonState;
749 } ContextMenuItem;
750
751 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
752 {
753 ContextMenuItem item = {.index = index, .alignX = alignX};
754 strncpy(item.text, text, 24);
755 return item;
756 }
757
758 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
759 {
760 ContextMenuItem item = {.index = index, .action = action, .args = args};
761 strncpy(item.text, text, 24);
762 return item;
763 }
764
765 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
766 {
767 const int itemHeight = 28;
768 const int itemSpacing = 1;
769 const int padding = 8;
770 int itemCount = 0;
771 for (int i = 0; menus[i].text[0]; i++)
772 {
773 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
774 }
775
776 Rectangle contextMenu = {0, 0, width,
777 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
778
779 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
780 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
781
782 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
783 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
784 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
785 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
786
787 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
788 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
789 const int itemX = contextMenu.x + itemSpacing;
790 const int itemWidth = contextMenu.width - itemSpacing * 2;
791 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
792 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
793 int status = 0;
794 for (int i = 0; menus[i].text[0]; i++)
795 {
796 if (menus[i].action)
797 {
798 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
799 {
800 status = menus[i].action(level, &menus[i].args);
801 }
802 }
803 else
804 {
805 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
806 }
807 }
808
809 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
810 {
811 return 1;
812 }
813
814 return status;
815 }
816
817 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
818 {
819 ContextMenuItem menu[12] = {0};
820 int menuCount = 0;
821 int menuIndex = 0;
822 if (tower)
823 {
824
825 if (tower) {
826 menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f);
827 }
828
829 // two texts, same line
830 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
831 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
832
833 if (tower->towerType != TOWER_TYPE_BASE)
834 {
835 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
836 (ContextMenuArgs){.tower = tower});
837 }
838
839 if (tower->towerType != TOWER_TYPE_BASE)
840 {
841
842 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
843 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
844 }
845 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
846 {
847 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
848 (ContextMenuArgs){.tower = tower});
849 }
850 }
851 else
852 {
853 menu[menuCount] = ContextMenuItemButton(menuIndex++,
854 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)),
855 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
856 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL);
857
858 menu[menuCount] = ContextMenuItemButton(menuIndex++,
859 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)),
860 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
861 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER);
862
863 menu[menuCount] = ContextMenuItemButton(menuIndex++,
864 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)),
865 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
866 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA);
867
868 menu[menuCount] = ContextMenuItemButton(menuIndex++,
869 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)),
870 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
871 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT);
872 }
873
874 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
875 {
876 level->placementContextMenuStatus = -1;
877 }
878 }
879
880 void DrawLevelBuildingState(Level *level)
881 {
882 // when the context menu is not active, we update the placement position
883 if (level->placementContextMenuStatus == 0)
884 {
885 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
886 float hitDistance = ray.position.y / -ray.direction.y;
887 float hitX = ray.direction.x * hitDistance + ray.position.x;
888 float hitY = ray.direction.z * hitDistance + ray.position.z;
889 level->placementX = (int)floorf(hitX + 0.5f);
890 level->placementY = (int)floorf(hitY + 0.5f);
891 }
892
893 // the currently hovered/selected tower
894 Tower *tower = TowerGetAt(level->placementX, level->placementY);
895 // show the range of the tower when hovering/selecting it
896 TowerUpdateRangeFade(tower, 0.0f);
897
898 BeginMode3D(level->camera);
899 DrawLevelGround(level);
900 PathFindingMapUpdate(0, 0);
901 TowerDraw();
902 EnemyDraw();
903 ProjectileDraw();
904 ParticleDraw();
905 DrawEnemyPaths(level);
906
907 guiState.isBlocked = 0;
908
909 // Hover rectangle, when the mouse is over the map
910 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
911 if (isHovering)
912 {
913 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
914 }
915
916 EndMode3D();
917
918 TowerDrawHealthBars(level->camera);
919
920 DrawTitle("Building phase");
921
922 // Draw the context menu when the context menu is active
923 if (level->placementContextMenuStatus >= 1)
924 {
925 float maxHitpoints = 0.0f;
926 float hp = 0.0f;
927 float damageFactor = 0.0f;
928 int32_t sellValue = 0;
929
930 if (tower)
931 {
932 maxHitpoints = TowerGetMaxHealth(tower);
933 hp = maxHitpoints - tower->damage;
934 damageFactor = 1.0f - tower->damage / maxHitpoints;
935 sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
936 }
937
938 ContextMenuItem menu[12] = {0};
939 int menuCount = 0;
940 int menuIndex = 0;
941 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
942 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
943
944 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
945 {
946 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
947 }
948 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
949 {
950 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
951 int costs = totalLevel * 4;
952 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
953 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
954 GetTowerName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
955 int buttonMenuIndex = menuIndex;
956 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
957 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
958 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
959 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
960 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
961 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
962
963 // check if buttons should be disabled
964 if (isMaxLevel || level->playerGold < costs)
965 {
966 for (int i = buttonMenuIndex; i < menuCount; i++)
967 {
968 menu[i].buttonState.isDisabled = 1;
969 }
970 }
971
972 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
973 {
974 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
975 }
976 }
977 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
978 {
979 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f);
980 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
981 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
982 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
983 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
984 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
985 {
986 level->placementContextMenuStatus = -1;
987 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
988 }
989 }
990 }
991
992 // Activate the context menu when the mouse is clicked and the context menu is not active
993 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
994 {
995 level->placementContextMenuStatus += 1;
996 }
997
998 if (level->placementContextMenuStatus == 0)
999 {
1000 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1001 {
1002 level->nextState = LEVEL_STATE_RESET;
1003 }
1004
1005 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1006 {
1007 level->nextState = LEVEL_STATE_BATTLE;
1008 }
1009
1010 }
1011 }
1012
1013 void InitBattleStateConditions(Level *level)
1014 {
1015 level->state = LEVEL_STATE_BATTLE;
1016 level->nextState = LEVEL_STATE_NONE;
1017 level->waveEndTimer = 0.0f;
1018 for (int i = 0; i < 10; i++)
1019 {
1020 EnemyWave *wave = &level->waves[i];
1021 wave->spawned = 0;
1022 wave->timeToSpawnNext = wave->delay;
1023 }
1024 }
1025
1026 void DrawLevelBattleState(Level *level)
1027 {
1028 BeginMode3D(level->camera);
1029 DrawLevelGround(level);
1030 TowerDraw();
1031 EnemyDraw();
1032 ProjectileDraw();
1033 ParticleDraw();
1034 guiState.isBlocked = 0;
1035 EndMode3D();
1036
1037 EnemyDrawHealthbars(level->camera);
1038 TowerDrawHealthBars(level->camera);
1039
1040 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1041 {
1042 level->nextState = LEVEL_STATE_RESET;
1043 }
1044
1045 int maxCount = 0;
1046 int remainingCount = 0;
1047 for (int i = 0; i < 10; i++)
1048 {
1049 EnemyWave *wave = &level->waves[i];
1050 if (wave->wave != level->currentWave)
1051 {
1052 continue;
1053 }
1054 maxCount += wave->count;
1055 remainingCount += wave->count - wave->spawned;
1056 }
1057 int aliveCount = EnemyCount();
1058 remainingCount += aliveCount;
1059
1060 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1061 DrawTitle(text);
1062 }
1063
1064 void DrawLevel(Level *level)
1065 {
1066 switch (level->state)
1067 {
1068 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1069 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1070 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1071 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1072 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1073 default: break;
1074 }
1075
1076 DrawLevelHud(level);
1077 }
1078
1079 void UpdateLevel(Level *level)
1080 {
1081 if (level->state == LEVEL_STATE_BATTLE)
1082 {
1083 int activeWaves = 0;
1084 for (int i = 0; i < 10; i++)
1085 {
1086 EnemyWave *wave = &level->waves[i];
1087 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1088 {
1089 continue;
1090 }
1091 activeWaves++;
1092 wave->timeToSpawnNext -= gameTime.deltaTime;
1093 if (wave->timeToSpawnNext <= 0.0f)
1094 {
1095 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1096 if (enemy)
1097 {
1098 wave->timeToSpawnNext = wave->interval;
1099 wave->spawned++;
1100 }
1101 }
1102 }
1103 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1104 level->waveEndTimer += gameTime.deltaTime;
1105 if (level->waveEndTimer >= 2.0f)
1106 {
1107 level->nextState = LEVEL_STATE_LOST_WAVE;
1108 }
1109 }
1110 else if (activeWaves == 0 && EnemyCount() == 0)
1111 {
1112 level->waveEndTimer += gameTime.deltaTime;
1113 if (level->waveEndTimer >= 2.0f)
1114 {
1115 level->nextState = LEVEL_STATE_WON_WAVE;
1116 }
1117 }
1118 }
1119
1120 PathFindingMapUpdate(0, 0);
1121 EnemyUpdate();
1122 TowerUpdate();
1123 ProjectileUpdate();
1124 ParticleUpdate();
1125
1126 if (level->nextState == LEVEL_STATE_RESET)
1127 {
1128 InitLevel(level);
1129 }
1130
1131 if (level->nextState == LEVEL_STATE_BATTLE)
1132 {
1133 InitBattleStateConditions(level);
1134 }
1135
1136 if (level->nextState == LEVEL_STATE_WON_WAVE)
1137 {
1138 level->currentWave++;
1139 level->state = LEVEL_STATE_WON_WAVE;
1140 }
1141
1142 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1143 {
1144 level->state = LEVEL_STATE_LOST_WAVE;
1145 }
1146
1147 if (level->nextState == LEVEL_STATE_BUILDING)
1148 {
1149 level->state = LEVEL_STATE_BUILDING;
1150 level->placementContextMenuStatus = 0;
1151 }
1152
1153 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1154 {
1155 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1156 level->placementTransitionPosition = (Vector2){
1157 level->placementX, level->placementY};
1158 // initialize the spring to the current position
1159 level->placementTowerSpring = (PhysicsPoint){
1160 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1161 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1162 };
1163 level->placementPhase = PLACEMENT_PHASE_STARTING;
1164 level->placementTimer = 0.0f;
1165 }
1166
1167 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1168 {
1169 // make something of this later
1170 InitLevel(level);
1171 }
1172
1173 level->nextState = LEVEL_STATE_NONE;
1174 }
1175
1176 float nextSpawnTime = 0.0f;
1177
1178 void ResetGame()
1179 {
1180 InitLevel(currentLevel);
1181 }
1182
1183 void InitGame()
1184 {
1185 TowerInit();
1186 EnemyInit();
1187 ProjectileInit();
1188 ParticleInit();
1189 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1190
1191 currentLevel = levels;
1192 InitLevel(currentLevel);
1193 }
1194
1195 //# Immediate GUI functions
1196
1197 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1198 {
1199 const float healthBarHeight = 6.0f;
1200 const float healthBarOffset = 15.0f;
1201 const float inset = 2.0f;
1202 const float innerWidth = healthBarWidth - inset * 2;
1203 const float innerHeight = healthBarHeight - inset * 2;
1204
1205 Vector2 screenPos = GetWorldToScreen(position, camera);
1206 screenPos = Vector2Add(screenPos, screenOffset);
1207 float centerX = screenPos.x - healthBarWidth * 0.5f;
1208 float topY = screenPos.y - healthBarOffset;
1209 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1210 float healthWidth = innerWidth * healthRatio;
1211 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1212 }
1213
1214 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1215 {
1216 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1217
1218 DrawTextEx(gameFontNormal, text, (Vector2){
1219 x + (width - textSize.x) * alignX,
1220 y + (height - textSize.y) * alignY
1221 }, gameFontNormal.baseSize, 1, textColor);
1222 }
1223
1224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1225 {
1226 Rectangle bounds = {x, y, width, height};
1227 int isPressed = 0;
1228 int isSelected = state && state->isSelected;
1229 int isDisabled = state && state->isDisabled;
1230 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1231 {
1232 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1233 {
1234 isPressed = 1;
1235 }
1236 guiState.isBlocked = 1;
1237 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1238 bounds, Vector2Zero(), 0, WHITE);
1239 }
1240 else
1241 {
1242 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1243 bounds, Vector2Zero(), 0, WHITE);
1244 }
1245 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1246 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1247 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1248 return isPressed;
1249 }
1250
1251 //# Main game loop
1252
1253 void GameUpdate()
1254 {
1255 UpdateLevel(currentLevel);
1256 }
1257
1258 int main(void)
1259 {
1260 int screenWidth, screenHeight;
1261 GetPreferredSize(&screenWidth, &screenHeight);
1262 InitWindow(screenWidth, screenHeight, "Tower defense");
1263 float gamespeed = 1.0f;
1264 int frameRate = 30;
1265 SetTargetFPS(30);
1266
1267 LoadAssets();
1268 InitGame();
1269
1270 float pause = 1.0f;
1271
1272 while (!WindowShouldClose())
1273 {
1274 if (IsPaused()) {
1275 // canvas is not visible in browser - do nothing
1276 continue;
1277 }
1278
1279 if (IsKeyPressed(KEY_F))
1280 {
1281 frameRate = (frameRate + 5) % 30;
1282 frameRate = frameRate < 10 ? 10 : frameRate;
1283 SetTargetFPS(frameRate);
1284 }
1285
1286 if (IsKeyPressed(KEY_T))
1287 {
1288 gamespeed += 0.1f;
1289 if (gamespeed > 1.05f) gamespeed = 0.1f;
1290 }
1291
1292 if (IsKeyPressed(KEY_P))
1293 {
1294 pause = pause > 0.5f ? 0.0f : 1.0f;
1295 }
1296
1297 float dt = GetFrameTime() * gamespeed * pause;
1298 // cap maximum delta time to 0.1 seconds to prevent large time steps
1299 if (dt > 0.1f) dt = 0.1f;
1300 gameTime.time += dt;
1301 gameTime.deltaTime = dt;
1302 gameTime.frameCount += 1;
1303
1304 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1305 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1306
1307 BeginDrawing();
1308 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1309
1310 GameUpdate();
1311 DrawLevel(currentLevel);
1312
1313 if (gamespeed != 1.0f)
1314 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1315 EndDrawing();
1316
1317 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1318 }
1319
1320 CloseWindow();
1321
1322 return 0;
1323 }
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 #define TOWER_MAX_STAGE 10
74
75 typedef struct TowerUpgradeState
76 {
77 uint8_t range;
78 uint8_t damage;
79 uint8_t speed;
80 } TowerUpgradeState;
81
82 typedef struct Tower
83 {
84 int16_t x, y;
85 uint8_t towerType;
86 TowerUpgradeState upgradeState;
87 Vector2 lastTargetPosition;
88 float cooldown;
89 float damage;
90 // alpha value for the range circle drawing
91 float drawRangeAlpha;
92 } Tower;
93
94 typedef struct GameTime
95 {
96 float time;
97 float deltaTime;
98 uint32_t frameCount;
99
100 float fixedDeltaTime;
101 // leaving the fixed time stepping to the update functions,
102 // we need to know the fixed time at the start of the frame
103 float fixedTimeStart;
104 // and the number of fixed steps that we have to make this frame
105 // The fixedTime is fixedTimeStart + n * fixedStepCount
106 uint8_t fixedStepCount;
107 } GameTime;
108
109 typedef struct ButtonState {
110 char isSelected;
111 char isDisabled;
112 } ButtonState;
113
114 typedef struct GUIState {
115 int isBlocked;
116 } GUIState;
117
118 typedef enum LevelState
119 {
120 LEVEL_STATE_NONE,
121 LEVEL_STATE_BUILDING,
122 LEVEL_STATE_BUILDING_PLACEMENT,
123 LEVEL_STATE_BATTLE,
124 LEVEL_STATE_WON_WAVE,
125 LEVEL_STATE_LOST_WAVE,
126 LEVEL_STATE_WON_LEVEL,
127 LEVEL_STATE_RESET,
128 } LevelState;
129
130 typedef struct EnemyWave {
131 uint8_t enemyType;
132 uint8_t wave;
133 uint16_t count;
134 float interval;
135 float delay;
136 Vector2 spawnPosition;
137
138 uint16_t spawned;
139 float timeToSpawnNext;
140 } EnemyWave;
141
142 #define ENEMY_MAX_WAVE_COUNT 10
143
144 typedef enum PlacementPhase
145 {
146 PLACEMENT_PHASE_STARTING,
147 PLACEMENT_PHASE_MOVING,
148 PLACEMENT_PHASE_PLACING,
149 } PlacementPhase;
150
151 typedef struct Level
152 {
153 int seed;
154 LevelState state;
155 LevelState nextState;
156 Camera3D camera;
157 int placementMode;
158 PlacementPhase placementPhase;
159 float placementTimer;
160
161 int16_t placementX;
162 int16_t placementY;
163 int8_t placementContextMenuStatus;
164 int8_t placementContextMenuType;
165
166 Vector2 placementTransitionPosition;
167 PhysicsPoint placementTowerSpring;
168
169 int initialGold;
170 int playerGold;
171
172 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
173 int currentWave;
174 float waveEndTimer;
175 } Level;
176
177 typedef struct DeltaSrc
178 {
179 char x, y;
180 } DeltaSrc;
181
182 typedef struct PathfindingMap
183 {
184 int width, height;
185 float scale;
186 float *distances;
187 long *towerIndex;
188 DeltaSrc *deltaSrc;
189 float maxDistance;
190 Matrix toMapSpace;
191 Matrix toWorldSpace;
192 } PathfindingMap;
193
194 // when we execute the pathfinding algorithm, we need to store the active nodes
195 // in a queue. Each node has a position, a distance from the start, and the
196 // position of the node that we came from.
197 typedef struct PathfindingNode
198 {
199 int16_t x, y, fromX, fromY;
200 float distance;
201 } PathfindingNode;
202
203 typedef struct EnemyId
204 {
205 uint16_t index;
206 uint16_t generation;
207 } EnemyId;
208
209 typedef struct EnemyClassConfig
210 {
211 float speed;
212 float health;
213 float shieldHealth;
214 float shieldDamageAbsorption;
215 float radius;
216 float maxAcceleration;
217 float requiredContactTime;
218 float explosionDamage;
219 float explosionRange;
220 float explosionPushbackPower;
221 int goldValue;
222 } EnemyClassConfig;
223
224 typedef struct Enemy
225 {
226 int16_t currentX, currentY;
227 int16_t nextX, nextY;
228 Vector2 simPosition;
229 Vector2 simVelocity;
230 uint16_t generation;
231 float walkedDistance;
232 float startMovingTime;
233 float damage, futureDamage;
234 float shieldDamage;
235 float contactTime;
236 uint8_t enemyType;
237 uint8_t movePathCount;
238 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
239 } Enemy;
240
241 // a unit that uses sprites to be drawn
242 #define SPRITE_UNIT_ANIMATION_COUNT 6
243 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
244 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
245 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
246
247 typedef struct SpriteAnimation
248 {
249 Rectangle srcRect;
250 Vector2 offset;
251 uint8_t animationId;
252 uint8_t frameCount;
253 uint8_t frameWidth;
254 float frameDuration;
255 } SpriteAnimation;
256
257 typedef struct SpriteUnit
258 {
259 float scale;
260 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
261 } SpriteUnit;
262
263 #define PROJECTILE_MAX_COUNT 1200
264 #define PROJECTILE_TYPE_NONE 0
265 #define PROJECTILE_TYPE_ARROW 1
266 #define PROJECTILE_TYPE_CATAPULT 2
267 #define PROJECTILE_TYPE_BALLISTA 3
268
269 typedef struct Projectile
270 {
271 uint8_t projectileType;
272 float shootTime;
273 float arrivalTime;
274 float distance;
275 Vector3 position;
276 Vector3 target;
277 Vector3 directionNormal;
278 EnemyId targetEnemy;
279 HitEffectConfig hitEffectConfig;
280 } Projectile;
281
282 //# Function declarations
283 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
284 int EnemyAddDamageRange(Vector2 position, float range, float damage);
285 int EnemyAddDamage(Enemy *enemy, float damage);
286
287 //# Enemy functions
288 void EnemyInit();
289 void EnemyDraw();
290 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
291 void EnemyUpdate();
292 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
293 float EnemyGetMaxHealth(Enemy *enemy);
294 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
295 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
296 EnemyId EnemyGetId(Enemy *enemy);
297 Enemy *EnemyTryResolve(EnemyId enemyId);
298 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
299 int EnemyAddDamage(Enemy *enemy, float damage);
300 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
301 int EnemyCount();
302 void EnemyDrawHealthbars(Camera3D camera);
303
304 //# Tower functions
305 void TowerInit();
306 float TowerGetMaxHealth(Tower *tower);
307 Tower *GetTowerByIndex(int index);
308 Tower *TowerGetAt(int16_t x, int16_t y);
309 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
310 Tower *GetTowerByType(uint8_t towerType);
311 int GetTowerCosts(uint8_t towerType);
312 const char *GetTowerName(uint8_t towerType);
313 float TowerGetMaxHealth(Tower *tower);
314 void TowerDraw();
315 void TowerDrawSingle(Tower tower);
316 void TowerDrawRange(Tower tower, float alpha);
317 void TowerUpdate();
318 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget);
319 void TowerDrawHealthBars(Camera3D camera);
320 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
321
322 //# Particles
323 void ParticleInit();
324 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
325 void ParticleUpdate();
326 void ParticleDraw();
327
328 //# Projectiles
329 void ProjectileInit();
330 void ProjectileDraw();
331 void ProjectileUpdate();
332 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
333
334 //# Pathfinding map
335 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
336 float PathFindingGetDistance(int mapX, int mapY);
337 Vector2 PathFindingGetGradient(Vector3 world);
338 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
339 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
340 void PathFindingMapDraw();
341
342 //# UI
343 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
344
345 //# Level
346 void DrawLevelGround(Level *level);
347 void DrawEnemyPath(Level *level, Color arrowColor);
348
349 //# variables
350 extern Level *currentLevel;
351 extern Enemy enemies[ENEMY_MAX_COUNT];
352 extern int enemyCount;
353 extern EnemyClassConfig enemyClassConfigs[];
354
355 extern GUIState guiState;
356 extern GameTime gameTime;
357 extern Tower towers[TOWER_MAX_COUNT];
358 extern int towerCount;
359
360 extern Texture2D palette, spriteSheet;
361
362 #endif
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 *GetTowerByIndex(int index)
193 {
194 if (index < 0 || index >= towerCount)
195 {
196 return 0;
197 }
198 return &towers[index];
199 }
200
201 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
202 {
203 if (towerCount >= TOWER_MAX_COUNT)
204 {
205 return 0;
206 }
207
208 Tower *tower = TowerGetAt(x, y);
209 if (tower)
210 {
211 return 0;
212 }
213
214 tower = &towers[towerCount++];
215 *tower = (Tower){
216 .x = x,
217 .y = y,
218 .towerType = towerType,
219 .cooldown = 0.0f,
220 .damage = 0.0f,
221 };
222 return tower;
223 }
224
225 Tower *GetTowerByType(uint8_t towerType)
226 {
227 for (int i = 0; i < towerCount; i++)
228 {
229 if (towers[i].towerType == towerType)
230 {
231 return &towers[i];
232 }
233 }
234 return 0;
235 }
236
237 const char *GetTowerName(uint8_t towerType)
238 {
239 return towerTypeConfigs[towerType].name;
240 }
241
242 int GetTowerCosts(uint8_t towerType)
243 {
244 return towerTypeConfigs[towerType].cost;
245 }
246
247 float TowerGetMaxHealth(Tower *tower)
248 {
249 return towerTypeConfigs[tower->towerType].maxHealth;
250 }
251
252 float GetTowerRange(Tower tower)
253 {
254 return towerTypeConfigs[tower.towerType].range;
255 }
256
257 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget)
258 {
259 // animate fade in and fade out of range drawing using framerate independent lerp
260 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime);
261 for (int i = 0; i < TOWER_MAX_COUNT; i++)
262 {
263 Tower *fadingTower = GetTowerByIndex(i);
264 if (!fadingTower)
265 {
266 break;
267 }
268 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget;
269 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp);
270 }
271 }
272
273 void TowerDrawRange(Tower tower, float alpha)
274 {
275 Color ringColor = (Color){255, 200, 100, 255};
276 const int rings = 4;
277 const float radiusOffset = 0.5f;
278 const float animationSpeed = 2.0f;
279 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
280 float radius = GetTowerRange(tower);
281 // base circle
282 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, radius, (Vector3){1, 0, 0}, 90,
283 Fade(ringColor, alpha));
284
285 for (int i = 1; i < rings; i++)
286 {
287 float t = ((float)i + animation) / (float)rings;
288 float r = Lerp(radius, radius - radiusOffset, t * t);
289 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
290 if (i == 1)
291 {
292 // fade out the outermost ring
293 a = animation;
294 }
295 a *= alpha;
296
297 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, r, (Vector3){1, 0, 0}, 90,
298 Fade(ringColor, a));
299 }
300 }
301
302 void TowerDrawSingle(Tower tower)
303 {
304 if (tower.towerType == TOWER_TYPE_NONE)
305 {
306 return;
307 }
308
309 if (tower.drawRangeAlpha > 2.0f/256.0f)
310 {
311 TowerDrawRange(tower, tower.drawRangeAlpha);
312 }
313
314 switch (tower.towerType)
315 {
316 case TOWER_TYPE_ARCHER:
317 {
318 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
319 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
320 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
321 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
322 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
323 }
324 break;
325 case TOWER_TYPE_BALLISTA:
326 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
327 break;
328 case TOWER_TYPE_CATAPULT:
329 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
330 break;
331 default:
332 if (towerModels[tower.towerType].materials)
333 {
334 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
335 } else {
336 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
337 }
338 break;
339 }
340 }
341
342 void TowerDraw()
343 {
344 for (int i = 0; i < towerCount; i++)
345 {
346 TowerDrawSingle(towers[i]);
347 }
348 }
349
350 void TowerUpdate()
351 {
352 for (int i = 0; i < towerCount; i++)
353 {
354 Tower *tower = &towers[i];
355 switch (tower->towerType)
356 {
357 case TOWER_TYPE_CATAPULT:
358 case TOWER_TYPE_BALLISTA:
359 case TOWER_TYPE_ARCHER:
360 TowerGunUpdate(tower);
361 break;
362 }
363 }
364 }
365
366 void TowerDrawHealthBars(Camera3D camera)
367 {
368 for (int i = 0; i < towerCount; i++)
369 {
370 Tower *tower = &towers[i];
371 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
372 {
373 continue;
374 }
375
376 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
377 float maxHealth = TowerGetMaxHealth(tower);
378 float health = maxHealth - tower->damage;
379 float healthRatio = health / maxHealth;
380
381 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
382 }
383 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 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
With the applied changes, we can now see the range of the tower when placing it, as well when hovering with the mouse over the tower or when opening the context menu.
The animation for the growing circles is once again a procedurally generated effect:
1 void TowerDrawRange(Tower tower, float alpha)
2 {
3 Color ringColor = (Color){255, 200, 100, 255};
4 const int rings = 4;
5 const float radiusOffset = 0.5f;
6 const float animationSpeed = 2.0f;
7 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
8 float radius = GetTowerRange(tower);
9 // base circle
10 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, radius, (Vector3){1, 0, 0}, 90,
11 Fade(ringColor, alpha));
12
13 for (int i = 1; i < rings; i++)
14 {
15 float t = ((float)i + animation) / (float)rings;
16 float r = Lerp(radius, radius - radiusOffset, t * t);
17 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
18 if (i == 1)
19 {
20 // fade out the outermost ring
21 a = animation;
22 }
23 a *= alpha;
24
25 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, r, (Vector3){1, 0, 0}, 90,
26 Fade(ringColor, a));
27 }
28 }
The way this works is to draw a few rings (4) that grow at a slowing pace. The outermost ring stays at the same size and marks the boundary of the tower's range. The animation repeats via fmodf(gameTime.time * animationSpeed, 1.0f). Since I explained the approach to animating things via functions, I won't go into detail here.
Now we have to replace the logic in the tower_system.c file to handle the range upgrade.
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 // calculate x and z rotation to align the model with the spring
575 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
576 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
577 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
578 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
579 float springLength = Vector3Length(towerUp);
580 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
581 float towerSquash = 1.0f / towerStretch;
582
583 Tower dummy = {
584 .towerType = level->placementMode,
585 };
586
587 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
588 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
589 {
590 rangeAlpha = 1.0f - rangeAlpha;
591 }
592 else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
593 {
594 rangeAlpha = 1.0f;
595 }
596
597 TowerDrawRange(dummy, rangeAlpha);
598
599 rlPushMatrix();
600 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
601
602 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
603 rlScalef(towerSquash, towerStretch, towerSquash);
604 TowerDrawSingle(dummy);
605 rlPopMatrix();
606
607
608 // draw a shadow for the tower
609 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
610 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
611 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
612
613
614 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
615 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
616 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
617 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
618
619 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
620 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
621 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
622 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
623 rlPopMatrix();
624
625 guiState.isBlocked = 0;
626
627 EndMode3D();
628
629 TowerDrawHealthBars(level->camera);
630
631 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
632 {
633 if (level->placementTimer > placementDuration)
634 {
635 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
636 // testing repairing
637 tower->damage = 2.5f;
638 level->playerGold -= GetTowerCosts(level->placementMode);
639 level->nextState = LEVEL_STATE_BUILDING;
640 level->placementMode = TOWER_TYPE_NONE;
641 }
642 }
643 else
644 {
645 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
646 {
647 level->nextState = LEVEL_STATE_BUILDING;
648 level->placementMode = TOWER_TYPE_NONE;
649 TraceLog(LOG_INFO, "Cancel building");
650 }
651
652 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
653 {
654 level->placementPhase = PLACEMENT_PHASE_PLACING;
655 level->placementTimer = 0.0f;
656 }
657 }
658 }
659
660 enum ContextMenuType
661 {
662 CONTEXT_MENU_TYPE_MAIN,
663 CONTEXT_MENU_TYPE_SELL_CONFIRM,
664 CONTEXT_MENU_TYPE_UPGRADE,
665 };
666
667 enum UpgradeType
668 {
669 UPGRADE_TYPE_SPEED,
670 UPGRADE_TYPE_DAMAGE,
671 UPGRADE_TYPE_RANGE,
672 };
673
674 typedef struct ContextMenuArgs
675 {
676 void *data;
677 uint8_t uint8;
678 int32_t int32;
679 Tower *tower;
680 } ContextMenuArgs;
681
682 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
683 {
684 uint8_t towerType = data->uint8;
685 level->placementMode = towerType;
686 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
687 return 1;
688 }
689
690 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
691 {
692 Tower *tower = data->tower;
693 int gold = data->int32;
694 level->playerGold += gold;
695 tower->towerType = TOWER_TYPE_NONE;
696 return 1;
697 }
698
699 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
700 {
701 return 1;
702 }
703
704 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
705 {
706 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
707 return 0;
708 }
709
710 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
711 {
712 Tower *tower = data->tower;
713 switch (data->uint8)
714 {
715 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
716 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
717 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
718 }
719 level->playerGold -= data->int32;
720 return 0;
721 }
722
723 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
724 {
725 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
726 return 0;
727 }
728
729 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
730 {
731 Tower *tower = data->tower;
732 if (level->playerGold >= 1)
733 {
734 level->playerGold -= 1;
735 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
736 }
737 return tower->damage == 0.0f;
738 }
739
740 typedef struct ContextMenuItem
741 {
742 uint8_t index;
743 char text[24];
744 float alignX;
745 int (*action)(Level*, ContextMenuArgs*);
746 void *data;
747 ContextMenuArgs args;
748 ButtonState buttonState;
749 } ContextMenuItem;
750
751 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
752 {
753 ContextMenuItem item = {.index = index, .alignX = alignX};
754 strncpy(item.text, text, 24);
755 return item;
756 }
757
758 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
759 {
760 ContextMenuItem item = {.index = index, .action = action, .args = args};
761 strncpy(item.text, text, 24);
762 return item;
763 }
764
765 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
766 {
767 const int itemHeight = 28;
768 const int itemSpacing = 1;
769 const int padding = 8;
770 int itemCount = 0;
771 for (int i = 0; menus[i].text[0]; i++)
772 {
773 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
774 }
775
776 Rectangle contextMenu = {0, 0, width,
777 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
778
779 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
780 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
781
782 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
783 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
784 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
785 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
786
787 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
788 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
789 const int itemX = contextMenu.x + itemSpacing;
790 const int itemWidth = contextMenu.width - itemSpacing * 2;
791 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
792 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
793 int status = 0;
794 for (int i = 0; menus[i].text[0]; i++)
795 {
796 if (menus[i].action)
797 {
798 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
799 {
800 status = menus[i].action(level, &menus[i].args);
801 }
802 }
803 else
804 {
805 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
806 }
807 }
808
809 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
810 {
811 return 1;
812 }
813
814 return status;
815 }
816
817 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
818 {
819 ContextMenuItem menu[12] = {0};
820 int menuCount = 0;
821 int menuIndex = 0;
822 if (tower)
823 {
824
825 if (tower) {
826 menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f);
827 }
828
829 // two texts, same line
830 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
831 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
832
833 if (tower->towerType != TOWER_TYPE_BASE)
834 {
835 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
836 (ContextMenuArgs){.tower = tower});
837 }
838
839 if (tower->towerType != TOWER_TYPE_BASE)
840 {
841
842 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
843 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
844 }
845 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
846 {
847 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
848 (ContextMenuArgs){.tower = tower});
849 }
850 }
851 else
852 {
853 menu[menuCount] = ContextMenuItemButton(menuIndex++,
854 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)),
855 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
856 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL);
857
858 menu[menuCount] = ContextMenuItemButton(menuIndex++,
859 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)),
860 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
861 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER);
862
863 menu[menuCount] = ContextMenuItemButton(menuIndex++,
864 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)),
865 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
866 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA);
867
868 menu[menuCount] = ContextMenuItemButton(menuIndex++,
869 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)),
870 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
871 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT);
872 }
873
874 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
875 {
876 level->placementContextMenuStatus = -1;
877 }
878 }
879
880 void DrawLevelBuildingState(Level *level)
881 {
882 // when the context menu is not active, we update the placement position
883 if (level->placementContextMenuStatus == 0)
884 {
885 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
886 float hitDistance = ray.position.y / -ray.direction.y;
887 float hitX = ray.direction.x * hitDistance + ray.position.x;
888 float hitY = ray.direction.z * hitDistance + ray.position.z;
889 level->placementX = (int)floorf(hitX + 0.5f);
890 level->placementY = (int)floorf(hitY + 0.5f);
891 }
892
893 // the currently hovered/selected tower
894 Tower *tower = TowerGetAt(level->placementX, level->placementY);
895 // show the range of the tower when hovering/selecting it
896 TowerUpdateRangeFade(tower, 0.0f);
897
898 BeginMode3D(level->camera);
899 DrawLevelGround(level);
900 PathFindingMapUpdate(0, 0);
901 TowerDraw();
902 EnemyDraw();
903 ProjectileDraw();
904 ParticleDraw();
905 DrawEnemyPaths(level);
906
907 guiState.isBlocked = 0;
908
909 // Hover rectangle, when the mouse is over the map
910 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
911 if (isHovering)
912 {
913 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
914 }
915
916 EndMode3D();
917
918 TowerDrawHealthBars(level->camera);
919
920 DrawTitle("Building phase");
921
922 // Draw the context menu when the context menu is active
923 if (level->placementContextMenuStatus >= 1)
924 {
925 float maxHitpoints = 0.0f;
926 float hp = 0.0f;
927 float damageFactor = 0.0f;
928 int32_t sellValue = 0;
929
930 if (tower)
931 {
932 maxHitpoints = TowerGetMaxHealth(tower);
933 hp = maxHitpoints - tower->damage;
934 damageFactor = 1.0f - tower->damage / maxHitpoints;
935 sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
936 }
937
938 ContextMenuItem menu[12] = {0};
939 int menuCount = 0;
940 int menuIndex = 0;
941 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
942 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
943
944 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
945 {
946 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
947 }
948 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
949 {
950 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
951 int costs = totalLevel * 4;
952 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
953 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
954 GetTowerName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
955 int buttonMenuIndex = menuIndex;
956 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
957 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
958 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
959 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
960 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
961 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
962
963 // check if buttons should be disabled
964 if (isMaxLevel || level->playerGold < costs)
965 {
966 for (int i = buttonMenuIndex; i < menuCount; i++)
967 {
968 menu[i].buttonState.isDisabled = 1;
969 }
970 }
971
972 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
973 {
974 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
975 }
976 }
977 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
978 {
979 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f);
980 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
981 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
982 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
983 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
984 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
985 {
986 level->placementContextMenuStatus = -1;
987 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
988 }
989 }
990 }
991
992 // Activate the context menu when the mouse is clicked and the context menu is not active
993 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
994 {
995 level->placementContextMenuStatus += 1;
996 }
997
998 if (level->placementContextMenuStatus == 0)
999 {
1000 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1001 {
1002 level->nextState = LEVEL_STATE_RESET;
1003 }
1004
1005 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1006 {
1007 level->nextState = LEVEL_STATE_BATTLE;
1008 }
1009
1010 }
1011 }
1012
1013 void InitBattleStateConditions(Level *level)
1014 {
1015 level->state = LEVEL_STATE_BATTLE;
1016 level->nextState = LEVEL_STATE_NONE;
1017 level->waveEndTimer = 0.0f;
1018 for (int i = 0; i < 10; i++)
1019 {
1020 EnemyWave *wave = &level->waves[i];
1021 wave->spawned = 0;
1022 wave->timeToSpawnNext = wave->delay;
1023 }
1024 }
1025
1026 void DrawLevelBattleState(Level *level)
1027 {
1028 BeginMode3D(level->camera);
1029 DrawLevelGround(level);
1030 TowerDraw();
1031 EnemyDraw();
1032 ProjectileDraw();
1033 ParticleDraw();
1034 guiState.isBlocked = 0;
1035 EndMode3D();
1036
1037 EnemyDrawHealthbars(level->camera);
1038 TowerDrawHealthBars(level->camera);
1039
1040 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1041 {
1042 level->nextState = LEVEL_STATE_RESET;
1043 }
1044
1045 int maxCount = 0;
1046 int remainingCount = 0;
1047 for (int i = 0; i < 10; i++)
1048 {
1049 EnemyWave *wave = &level->waves[i];
1050 if (wave->wave != level->currentWave)
1051 {
1052 continue;
1053 }
1054 maxCount += wave->count;
1055 remainingCount += wave->count - wave->spawned;
1056 }
1057 int aliveCount = EnemyCount();
1058 remainingCount += aliveCount;
1059
1060 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1061 DrawTitle(text);
1062 }
1063
1064 void DrawLevel(Level *level)
1065 {
1066 switch (level->state)
1067 {
1068 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1069 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1070 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1071 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1072 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1073 default: break;
1074 }
1075
1076 DrawLevelHud(level);
1077 }
1078
1079 void UpdateLevel(Level *level)
1080 {
1081 if (level->state == LEVEL_STATE_BATTLE)
1082 {
1083 int activeWaves = 0;
1084 for (int i = 0; i < 10; i++)
1085 {
1086 EnemyWave *wave = &level->waves[i];
1087 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1088 {
1089 continue;
1090 }
1091 activeWaves++;
1092 wave->timeToSpawnNext -= gameTime.deltaTime;
1093 if (wave->timeToSpawnNext <= 0.0f)
1094 {
1095 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1096 if (enemy)
1097 {
1098 wave->timeToSpawnNext = wave->interval;
1099 wave->spawned++;
1100 }
1101 }
1102 }
1103 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1104 level->waveEndTimer += gameTime.deltaTime;
1105 if (level->waveEndTimer >= 2.0f)
1106 {
1107 level->nextState = LEVEL_STATE_LOST_WAVE;
1108 }
1109 }
1110 else if (activeWaves == 0 && EnemyCount() == 0)
1111 {
1112 level->waveEndTimer += gameTime.deltaTime;
1113 if (level->waveEndTimer >= 2.0f)
1114 {
1115 level->nextState = LEVEL_STATE_WON_WAVE;
1116 }
1117 }
1118 }
1119
1120 PathFindingMapUpdate(0, 0);
1121 EnemyUpdate();
1122 TowerUpdate();
1123 ProjectileUpdate();
1124 ParticleUpdate();
1125
1126 if (level->nextState == LEVEL_STATE_RESET)
1127 {
1128 InitLevel(level);
1129 }
1130
1131 if (level->nextState == LEVEL_STATE_BATTLE)
1132 {
1133 InitBattleStateConditions(level);
1134 }
1135
1136 if (level->nextState == LEVEL_STATE_WON_WAVE)
1137 {
1138 level->currentWave++;
1139 level->state = LEVEL_STATE_WON_WAVE;
1140 }
1141
1142 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1143 {
1144 level->state = LEVEL_STATE_LOST_WAVE;
1145 }
1146
1147 if (level->nextState == LEVEL_STATE_BUILDING)
1148 {
1149 level->state = LEVEL_STATE_BUILDING;
1150 level->placementContextMenuStatus = 0;
1151 }
1152
1153 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1154 {
1155 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1156 level->placementTransitionPosition = (Vector2){
1157 level->placementX, level->placementY};
1158 // initialize the spring to the current position
1159 level->placementTowerSpring = (PhysicsPoint){
1160 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1161 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1162 };
1163 level->placementPhase = PLACEMENT_PHASE_STARTING;
1164 level->placementTimer = 0.0f;
1165 }
1166
1167 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1168 {
1169 // make something of this later
1170 InitLevel(level);
1171 }
1172
1173 level->nextState = LEVEL_STATE_NONE;
1174 }
1175
1176 float nextSpawnTime = 0.0f;
1177
1178 void ResetGame()
1179 {
1180 InitLevel(currentLevel);
1181 }
1182
1183 void InitGame()
1184 {
1185 TowerInit();
1186 EnemyInit();
1187 ProjectileInit();
1188 ParticleInit();
1189 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1190
1191 currentLevel = levels;
1192 InitLevel(currentLevel);
1193 }
1194
1195 //# Immediate GUI functions
1196
1197 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1198 {
1199 const float healthBarHeight = 6.0f;
1200 const float healthBarOffset = 15.0f;
1201 const float inset = 2.0f;
1202 const float innerWidth = healthBarWidth - inset * 2;
1203 const float innerHeight = healthBarHeight - inset * 2;
1204
1205 Vector2 screenPos = GetWorldToScreen(position, camera);
1206 screenPos = Vector2Add(screenPos, screenOffset);
1207 float centerX = screenPos.x - healthBarWidth * 0.5f;
1208 float topY = screenPos.y - healthBarOffset;
1209 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1210 float healthWidth = innerWidth * healthRatio;
1211 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1212 }
1213
1214 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1215 {
1216 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1217
1218 DrawTextEx(gameFontNormal, text, (Vector2){
1219 x + (width - textSize.x) * alignX,
1220 y + (height - textSize.y) * alignY
1221 }, gameFontNormal.baseSize, 1, textColor);
1222 }
1223
1224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1225 {
1226 Rectangle bounds = {x, y, width, height};
1227 int isPressed = 0;
1228 int isSelected = state && state->isSelected;
1229 int isDisabled = state && state->isDisabled;
1230 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1231 {
1232 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1233 {
1234 isPressed = 1;
1235 }
1236 guiState.isBlocked = 1;
1237 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1238 bounds, Vector2Zero(), 0, WHITE);
1239 }
1240 else
1241 {
1242 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1243 bounds, Vector2Zero(), 0, WHITE);
1244 }
1245 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1246 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1247 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1248 return isPressed;
1249 }
1250
1251 //# Main game loop
1252
1253 void GameUpdate()
1254 {
1255 UpdateLevel(currentLevel);
1256 }
1257
1258 int main(void)
1259 {
1260 int screenWidth, screenHeight;
1261 GetPreferredSize(&screenWidth, &screenHeight);
1262 InitWindow(screenWidth, screenHeight, "Tower defense");
1263 float gamespeed = 1.0f;
1264 int frameRate = 30;
1265 SetTargetFPS(30);
1266
1267 LoadAssets();
1268 InitGame();
1269
1270 float pause = 1.0f;
1271
1272 while (!WindowShouldClose())
1273 {
1274 if (IsPaused()) {
1275 // canvas is not visible in browser - do nothing
1276 continue;
1277 }
1278
1279 if (IsKeyPressed(KEY_F))
1280 {
1281 frameRate = (frameRate + 5) % 30;
1282 frameRate = frameRate < 10 ? 10 : frameRate;
1283 SetTargetFPS(frameRate);
1284 }
1285
1286 if (IsKeyPressed(KEY_T))
1287 {
1288 gamespeed += 0.1f;
1289 if (gamespeed > 1.05f) gamespeed = 0.1f;
1290 }
1291
1292 if (IsKeyPressed(KEY_P))
1293 {
1294 pause = pause > 0.5f ? 0.0f : 1.0f;
1295 }
1296
1297 float dt = GetFrameTime() * gamespeed * pause;
1298 // cap maximum delta time to 0.1 seconds to prevent large time steps
1299 if (dt > 0.1f) dt = 0.1f;
1300 gameTime.time += dt;
1301 gameTime.deltaTime = dt;
1302 gameTime.frameCount += 1;
1303
1304 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1305 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1306
1307 BeginDrawing();
1308 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1309
1310 GameUpdate();
1311 DrawLevel(currentLevel);
1312
1313 if (gamespeed != 1.0f)
1314 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1315 EndDrawing();
1316
1317 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1318 }
1319
1320 CloseWindow();
1321
1322 return 0;
1323 }
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 maxUpgradeRange;
65 float projectileSpeed;
66
67 uint8_t cost;
68 uint8_t projectileType;
69 uint16_t maxHealth;
70
71 HitEffectConfig hitEffect;
72 } TowerTypeConfig;
73
74 #define TOWER_MAX_STAGE 10
75
76 typedef struct TowerUpgradeState
77 {
78 uint8_t range;
79 uint8_t damage;
80 uint8_t speed;
81 } TowerUpgradeState;
82
83 typedef struct Tower
84 {
85 int16_t x, y;
86 uint8_t towerType;
87 TowerUpgradeState upgradeState;
88 Vector2 lastTargetPosition;
89 float cooldown;
90 float damage;
91 // alpha value for the range circle drawing
92 float drawRangeAlpha;
93 } Tower;
94
95 typedef struct GameTime
96 {
97 float time;
98 float deltaTime;
99 uint32_t frameCount;
100
101 float fixedDeltaTime;
102 // leaving the fixed time stepping to the update functions,
103 // we need to know the fixed time at the start of the frame
104 float fixedTimeStart;
105 // and the number of fixed steps that we have to make this frame
106 // The fixedTime is fixedTimeStart + n * fixedStepCount
107 uint8_t fixedStepCount;
108 } GameTime;
109
110 typedef struct ButtonState {
111 char isSelected;
112 char isDisabled;
113 } ButtonState;
114
115 typedef struct GUIState {
116 int isBlocked;
117 } GUIState;
118
119 typedef enum LevelState
120 {
121 LEVEL_STATE_NONE,
122 LEVEL_STATE_BUILDING,
123 LEVEL_STATE_BUILDING_PLACEMENT,
124 LEVEL_STATE_BATTLE,
125 LEVEL_STATE_WON_WAVE,
126 LEVEL_STATE_LOST_WAVE,
127 LEVEL_STATE_WON_LEVEL,
128 LEVEL_STATE_RESET,
129 } LevelState;
130
131 typedef struct EnemyWave {
132 uint8_t enemyType;
133 uint8_t wave;
134 uint16_t count;
135 float interval;
136 float delay;
137 Vector2 spawnPosition;
138
139 uint16_t spawned;
140 float timeToSpawnNext;
141 } EnemyWave;
142
143 #define ENEMY_MAX_WAVE_COUNT 10
144
145 typedef enum PlacementPhase
146 {
147 PLACEMENT_PHASE_STARTING,
148 PLACEMENT_PHASE_MOVING,
149 PLACEMENT_PHASE_PLACING,
150 } PlacementPhase;
151
152 typedef struct Level
153 {
154 int seed;
155 LevelState state;
156 LevelState nextState;
157 Camera3D camera;
158 int placementMode;
159 PlacementPhase placementPhase;
160 float placementTimer;
161
162 int16_t placementX;
163 int16_t placementY;
164 int8_t placementContextMenuStatus;
165 int8_t placementContextMenuType;
166
167 Vector2 placementTransitionPosition;
168 PhysicsPoint placementTowerSpring;
169
170 int initialGold;
171 int playerGold;
172
173 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
174 int currentWave;
175 float waveEndTimer;
176 } Level;
177
178 typedef struct DeltaSrc
179 {
180 char x, y;
181 } DeltaSrc;
182
183 typedef struct PathfindingMap
184 {
185 int width, height;
186 float scale;
187 float *distances;
188 long *towerIndex;
189 DeltaSrc *deltaSrc;
190 float maxDistance;
191 Matrix toMapSpace;
192 Matrix toWorldSpace;
193 } PathfindingMap;
194
195 // when we execute the pathfinding algorithm, we need to store the active nodes
196 // in a queue. Each node has a position, a distance from the start, and the
197 // position of the node that we came from.
198 typedef struct PathfindingNode
199 {
200 int16_t x, y, fromX, fromY;
201 float distance;
202 } PathfindingNode;
203
204 typedef struct EnemyId
205 {
206 uint16_t index;
207 uint16_t generation;
208 } EnemyId;
209
210 typedef struct EnemyClassConfig
211 {
212 float speed;
213 float health;
214 float shieldHealth;
215 float shieldDamageAbsorption;
216 float radius;
217 float maxAcceleration;
218 float requiredContactTime;
219 float explosionDamage;
220 float explosionRange;
221 float explosionPushbackPower;
222 int goldValue;
223 } EnemyClassConfig;
224
225 typedef struct Enemy
226 {
227 int16_t currentX, currentY;
228 int16_t nextX, nextY;
229 Vector2 simPosition;
230 Vector2 simVelocity;
231 uint16_t generation;
232 float walkedDistance;
233 float startMovingTime;
234 float damage, futureDamage;
235 float shieldDamage;
236 float contactTime;
237 uint8_t enemyType;
238 uint8_t movePathCount;
239 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
240 } Enemy;
241
242 // a unit that uses sprites to be drawn
243 #define SPRITE_UNIT_ANIMATION_COUNT 6
244 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
245 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
246 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
247
248 typedef struct SpriteAnimation
249 {
250 Rectangle srcRect;
251 Vector2 offset;
252 uint8_t animationId;
253 uint8_t frameCount;
254 uint8_t frameWidth;
255 float frameDuration;
256 } SpriteAnimation;
257
258 typedef struct SpriteUnit
259 {
260 float scale;
261 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
262 } SpriteUnit;
263
264 #define PROJECTILE_MAX_COUNT 1200
265 #define PROJECTILE_TYPE_NONE 0
266 #define PROJECTILE_TYPE_ARROW 1
267 #define PROJECTILE_TYPE_CATAPULT 2
268 #define PROJECTILE_TYPE_BALLISTA 3
269
270 typedef struct Projectile
271 {
272 uint8_t projectileType;
273 float shootTime;
274 float arrivalTime;
275 float distance;
276 Vector3 position;
277 Vector3 target;
278 Vector3 directionNormal;
279 EnemyId targetEnemy;
280 HitEffectConfig hitEffectConfig;
281 } Projectile;
282
283 //# Function declarations
284 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
285 int EnemyAddDamageRange(Vector2 position, float range, float damage);
286 int EnemyAddDamage(Enemy *enemy, float damage);
287
288 //# Enemy functions
289 void EnemyInit();
290 void EnemyDraw();
291 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
292 void EnemyUpdate();
293 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
294 float EnemyGetMaxHealth(Enemy *enemy);
295 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
296 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
297 EnemyId EnemyGetId(Enemy *enemy);
298 Enemy *EnemyTryResolve(EnemyId enemyId);
299 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
300 int EnemyAddDamage(Enemy *enemy, float damage);
301 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
302 int EnemyCount();
303 void EnemyDrawHealthbars(Camera3D camera);
304
305 //# Tower functions
306 void TowerInit();
307 float TowerGetMaxHealth(Tower *tower);
308 Tower *GetTowerByIndex(int index);
309 Tower *TowerGetAt(int16_t x, int16_t y);
310 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
311 Tower *GetTowerByType(uint8_t towerType);
312 int GetTowerCosts(uint8_t towerType);
313 const char *GetTowerName(uint8_t towerType);
314 float GetTowerRange(Tower tower);
315 float TowerGetMaxHealth(Tower *tower);
316 void TowerDraw();
317 void TowerDrawSingle(Tower tower);
318 void TowerDrawRange(Tower tower, float alpha);
319 void TowerUpdate();
320 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget);
321 void TowerDrawHealthBars(Camera3D camera);
322 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
323
324 //# Particles
325 void ParticleInit();
326 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
327 void ParticleUpdate();
328 void ParticleDraw();
329
330 //# Projectiles
331 void ProjectileInit();
332 void ProjectileDraw();
333 void ProjectileUpdate();
334 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
335
336 //# Pathfinding map
337 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
338 float PathFindingGetDistance(int mapX, int mapY);
339 Vector2 PathFindingGetGradient(Vector3 world);
340 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
341 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
342 void PathFindingMapDraw();
343
344 //# UI
345 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
346
347 //# Level
348 void DrawLevelGround(Level *level);
349 void DrawEnemyPath(Level *level, Color arrowColor);
350
351 //# variables
352 extern Level *currentLevel;
353 extern Enemy enemies[ENEMY_MAX_COUNT];
354 extern int enemyCount;
355 extern EnemyClassConfig enemyClassConfigs[];
356
357 extern GUIState guiState;
358 extern GameTime gameTime;
359 extern Tower towers[TOWER_MAX_COUNT];
360 extern int towerCount;
361
362 extern Texture2D palette, spriteSheet;
363
364 #endif
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 .maxUpgradeRange = 5.0f,
14 .cost = 6,
15 .maxHealth = 10,
16 .projectileSpeed = 4.0f,
17 .projectileType = PROJECTILE_TYPE_ARROW,
18 .hitEffect = {
19 .damage = 3.0f,
20 }
21 },
22 [TOWER_TYPE_BALLISTA] = {
23 .name = "Ballista",
24 .cooldown = 1.5f,
25 .range = 6.0f,
26 .maxUpgradeRange = 8.0f,
27 .cost = 9,
28 .maxHealth = 10,
29 .projectileSpeed = 10.0f,
30 .projectileType = PROJECTILE_TYPE_BALLISTA,
31 .hitEffect = {
32 .damage = 8.0f,
33 .pushbackPowerDistance = 0.25f,
34 }
35 },
36 [TOWER_TYPE_CATAPULT] = {
37 .name = "Catapult",
38 .cooldown = 1.7f,
39 .range = 5.0f,
40 .maxUpgradeRange = 7.0f,
41 .cost = 10,
42 .maxHealth = 10,
43 .projectileSpeed = 3.0f,
44 .projectileType = PROJECTILE_TYPE_CATAPULT,
45 .hitEffect = {
46 .damage = 2.0f,
47 .areaDamageRadius = 1.75f,
48 }
49 },
50 [TOWER_TYPE_WALL] = {
51 .name = "Wall",
52 .cost = 2,
53 .maxHealth = 10,
54 },
55 };
56
57 Tower towers[TOWER_MAX_COUNT];
58 int towerCount = 0;
59
60 Model towerModels[TOWER_TYPE_COUNT];
61
62 // definition of our archer unit
63 SpriteUnit archerUnit = {
64 .animations[0] = {
65 .srcRect = {0, 0, 16, 16},
66 .offset = {7, 1},
67 .frameCount = 1,
68 .frameDuration = 0.0f,
69 },
70 .animations[1] = {
71 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
72 .srcRect = {16, 0, 6, 16},
73 .offset = {8, 0},
74 },
75 .animations[2] = {
76 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
77 .srcRect = {22, 0, 11, 16},
78 .offset = {10, 0},
79 },
80 };
81
82 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
83 {
84 float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
85 float xScale = flip ? -1.0f : 1.0f;
86 Camera3D camera = currentLevel->camera;
87 float size = 0.5f * unitScale;
88 // we want the sprite to face the camera, so we need to calculate the up vector
89 Vector3 forward = Vector3Subtract(camera.target, camera.position);
90 Vector3 up = {0, 1, 0};
91 Vector3 right = Vector3CrossProduct(forward, up);
92 up = Vector3Normalize(Vector3CrossProduct(right, forward));
93
94 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
95 {
96 SpriteAnimation anim = unit.animations[i];
97 if (anim.animationId != phase && anim.animationId != 0)
98 {
99 continue;
100 }
101 Rectangle srcRect = anim.srcRect;
102 if (anim.frameCount > 1)
103 {
104 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
105 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
106 }
107 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
108 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
109
110 if (flip)
111 {
112 srcRect.x += srcRect.width;
113 srcRect.width = -srcRect.width;
114 offset.x = scale.x - offset.x;
115 }
116 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
117 // move the sprite slightly towards the camera to avoid z-fighting
118 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
119 }
120 }
121
122 void TowerInit()
123 {
124 for (int i = 0; i < TOWER_MAX_COUNT; i++)
125 {
126 towers[i] = (Tower){0};
127 }
128 towerCount = 0;
129
130 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
131 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
132
133 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
134 {
135 if (towerModels[i].materials)
136 {
137 // assign the palette texture to the material of the model (0 is not used afaik)
138 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
139 }
140 }
141 }
142
143 static void TowerGunUpdate(Tower *tower)
144 {
145 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
146 if (tower->cooldown <= 0.0f)
147 {
148 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, GetTowerRange(*tower));
149 if (enemy)
150 {
151 tower->cooldown = config.cooldown;
152 // shoot the enemy; determine future position of the enemy
153 float bulletSpeed = config.projectileSpeed;
154 Vector2 velocity = enemy->simVelocity;
155 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
156 Vector2 towerPosition = {tower->x, tower->y};
157 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
158 for (int i = 0; i < 8; i++) {
159 velocity = enemy->simVelocity;
160 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
161 float distance = Vector2Distance(towerPosition, futurePosition);
162 float eta2 = distance / bulletSpeed;
163 if (fabs(eta - eta2) < 0.01f) {
164 break;
165 }
166 eta = (eta2 + eta) * 0.5f;
167 }
168
169 ProjectileTryAdd(config.projectileType, enemy,
170 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
171 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
172 bulletSpeed, config.hitEffect);
173 enemy->futureDamage += config.hitEffect.damage;
174 tower->lastTargetPosition = futurePosition;
175 }
176 }
177 else
178 {
179 tower->cooldown -= gameTime.deltaTime;
180 }
181 }
182
183 Tower *TowerGetAt(int16_t x, int16_t y)
184 {
185 for (int i = 0; i < towerCount; i++)
186 {
187 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
188 {
189 return &towers[i];
190 }
191 }
192 return 0;
193 }
194
195 Tower *GetTowerByIndex(int index)
196 {
197 if (index < 0 || index >= towerCount)
198 {
199 return 0;
200 }
201 return &towers[index];
202 }
203
204 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
205 {
206 if (towerCount >= TOWER_MAX_COUNT)
207 {
208 return 0;
209 }
210
211 Tower *tower = TowerGetAt(x, y);
212 if (tower)
213 {
214 return 0;
215 }
216
217 tower = &towers[towerCount++];
218 *tower = (Tower){
219 .x = x,
220 .y = y,
221 .towerType = towerType,
222 .cooldown = 0.0f,
223 .damage = 0.0f,
224 };
225 return tower;
226 }
227
228 Tower *GetTowerByType(uint8_t towerType)
229 {
230 for (int i = 0; i < towerCount; i++)
231 {
232 if (towers[i].towerType == towerType)
233 {
234 return &towers[i];
235 }
236 }
237 return 0;
238 }
239
240 const char *GetTowerName(uint8_t towerType)
241 {
242 return towerTypeConfigs[towerType].name;
243 }
244
245 int GetTowerCosts(uint8_t towerType)
246 {
247 return towerTypeConfigs[towerType].cost;
248 }
249
250 float TowerGetMaxHealth(Tower *tower)
251 {
252 return towerTypeConfigs[tower->towerType].maxHealth;
253 }
254
255 float GetTowerRange(Tower tower)
256 {
257 float range = towerTypeConfigs[tower.towerType].range;
258 float maxUpgradeRange = towerTypeConfigs[tower.towerType].maxUpgradeRange;
259 if (tower.upgradeState.range > 0)
260 {
261 range = Lerp(range, maxUpgradeRange, tower.upgradeState.range / (float)TOWER_MAX_STAGE);
262 }
263 return range;
264 }
265
266 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget)
267 {
268 // animate fade in and fade out of range drawing using framerate independent lerp
269 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime);
270 for (int i = 0; i < TOWER_MAX_COUNT; i++)
271 {
272 Tower *fadingTower = GetTowerByIndex(i);
273 if (!fadingTower)
274 {
275 break;
276 }
277 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget;
278 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp);
279 }
280 }
281
282 void TowerDrawRange(Tower tower, float alpha)
283 {
284 Color ringColor = (Color){255, 200, 100, 255};
285 const int rings = 4;
286 const float radiusOffset = 0.5f;
287 const float animationSpeed = 2.0f;
288 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
289 float radius = GetTowerRange(tower);
290 // base circle
291 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, radius, (Vector3){1, 0, 0}, 90,
292 Fade(ringColor, alpha));
293
294 for (int i = 1; i < rings; i++)
295 {
296 float t = ((float)i + animation) / (float)rings;
297 float r = Lerp(radius, radius - radiusOffset, t * t);
298 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
299 if (i == 1)
300 {
301 // fade out the outermost ring
302 a = animation;
303 }
304 a *= alpha;
305
306 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, r, (Vector3){1, 0, 0}, 90,
307 Fade(ringColor, a));
308 }
309 }
310
311 void TowerDrawSingle(Tower tower)
312 {
313 if (tower.towerType == TOWER_TYPE_NONE)
314 {
315 return;
316 }
317
318 if (tower.drawRangeAlpha > 2.0f/256.0f)
319 {
320 TowerDrawRange(tower, tower.drawRangeAlpha);
321 }
322
323 switch (tower.towerType)
324 {
325 case TOWER_TYPE_ARCHER:
326 {
327 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
328 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
329 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
330 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
331 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
332 }
333 break;
334 case TOWER_TYPE_BALLISTA:
335 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
336 break;
337 case TOWER_TYPE_CATAPULT:
338 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
339 break;
340 default:
341 if (towerModels[tower.towerType].materials)
342 {
343 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
344 } else {
345 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
346 }
347 break;
348 }
349 }
350
351 void TowerDraw()
352 {
353 for (int i = 0; i < towerCount; i++)
354 {
355 TowerDrawSingle(towers[i]);
356 }
357 }
358
359 void TowerUpdate()
360 {
361 for (int i = 0; i < towerCount; i++)
362 {
363 Tower *tower = &towers[i];
364 switch (tower->towerType)
365 {
366 case TOWER_TYPE_CATAPULT:
367 case TOWER_TYPE_BALLISTA:
368 case TOWER_TYPE_ARCHER:
369 TowerGunUpdate(tower);
370 break;
371 }
372 }
373 }
374
375 void TowerDrawHealthBars(Camera3D camera)
376 {
377 for (int i = 0; i < towerCount; i++)
378 {
379 Tower *tower = &towers[i];
380 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
381 {
382 continue;
383 }
384
385 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
386 float maxHealth = TowerGetMaxHealth(tower);
387 float health = maxHealth - tower->damage;
388 float healthRatio = health / maxHealth;
389
390 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
391 }
392 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 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 range function of the tower returns now the range depending on the upgrade level. It is a simple linear interpolation (Lerp) between the base range and the maximum range. The range is properly used and displayed in the UI, just like intended.
But looking at the various function signatures of the tower system ... :
1 void TowerInit();
2 float TowerGetMaxHealth(Tower *tower);
3 Tower *GetTowerByIndex(int index);
4 Tower *TowerGetAt(int16_t x, int16_t y);
5 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
6 Tower *GetTowerByType(uint8_t towerType);
7 int GetTowerCosts(uint8_t towerType);
8 const char *GetTowerName(uint8_t towerType);
9 float GetTowerRange(Tower tower);
10 float TowerGetMaxHealth(Tower *tower);
11 void TowerDraw();
12 void TowerDrawSingle(Tower tower);
13 void TowerDrawRange(Tower tower, float alpha);
14 void TowerUpdate();
15 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget);
16 void TowerDrawHealthBars(Camera3D camera);
... it is clear that there is quite some inconsistency in naming and passing arguments:
- Some functions take a pointer to a tower, some take a tower as value
- Some function names start with Tower, some with a verb
Accumulating such inconsistencies is common and it just happens. Cleaning this up from time to time is a good idea, just like thinking about why this happenend in the first place.
Naming the functions consistently helps navigating the codebase. Raylib uses the convention to start function names with a verb. My own practice I use to follow is to start function names with the system they primarily belong to, which is typically a noun. I like that when typing "Tower" in the editor, I get a list of all functions related to the tower system. Another point is visual aesthetics to me:
1 void TowerInit();
2 void TowerUpdate();
3 void TowerDraw();
4 float TowerGetRange(Tower *tower);
5
6 // vs
7
8 void InitTower();
9 void UpdateTower();
10 void DrawTower();
11 float GetTowerRange(Tower *tower);
I find the first version more pleasing to the eye since the verbs after the nouns start most often at the same column since they all start with the same prefix ( void Tower... ).
But this is just a personal preference; people have different tastes, which is fine. Getting confused by inconsistent naming makes it however harder to use the code. So whatever convention you choose, stick to it. The majority of functions in the tower system start with Tower, so I will stick to that.
Another point is the inconsistency in passing the tower as a pointer or as a value. Again, it is common in raylib's codebase to pass structs as values. Using pointers can be confusing for beginners, which is why raylib does it this way.
Since most functions in the tower system take a pointer to a tower, I will stick to that as well. Pass by pointer tends to be more efficient since the struct is not copied.
There is much that could be said about this topic, but I think most of the arguments are quite subjective when it comes to naming. The most important thing is to be consistent. Since quite a few functions modify the tower struct, passing pointers feels more natural and sticking to a single convention makes the code easier to remember and use. So let's do the cleanups for the tower module:
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 TowerDrawAll();
205 EnemyDraw();
206 ProjectileDraw();
207 ParticleDraw();
208 guiState.isBlocked = 0;
209 EndMode3D();
210
211 TowerDrawAllHealthBars(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 TowerDrawAll();
239 EnemyDraw();
240 ProjectileDraw();
241 ParticleDraw();
242 guiState.isBlocked = 0;
243 EndMode3D();
244
245 TowerDrawAllHealthBars(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 = TowerTypeGetCosts(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 TowerDrawAll();
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 // calculate x and z rotation to align the model with the spring
575 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
576 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
577 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
578 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
579 float springLength = Vector3Length(towerUp);
580 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
581 float towerSquash = 1.0f / towerStretch;
582
583 Tower dummy = {
584 .towerType = level->placementMode,
585 };
586
587 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
588 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
589 {
590 rangeAlpha = 1.0f - rangeAlpha;
591 }
592 else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
593 {
594 rangeAlpha = 1.0f;
595 }
596
597 TowerDrawRange(&dummy, rangeAlpha);
598
599 rlPushMatrix();
600 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
601
602 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
603 rlScalef(towerSquash, towerStretch, towerSquash);
604 TowerDrawModel(&dummy);
605 rlPopMatrix();
606
607
608 // draw a shadow for the tower
609 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
610 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
611 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
612
613
614 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
615 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
616 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
617 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
618
619 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
620 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
621 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
622 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
623 rlPopMatrix();
624
625 guiState.isBlocked = 0;
626
627 EndMode3D();
628
629 TowerDrawAllHealthBars(level->camera);
630
631 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
632 {
633 if (level->placementTimer > placementDuration)
634 {
635 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
636 // testing repairing
637 tower->damage = 2.5f;
638 level->playerGold -= TowerTypeGetCosts(level->placementMode);
639 level->nextState = LEVEL_STATE_BUILDING;
640 level->placementMode = TOWER_TYPE_NONE;
641 }
642 }
643 else
644 {
645 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
646 {
647 level->nextState = LEVEL_STATE_BUILDING;
648 level->placementMode = TOWER_TYPE_NONE;
649 TraceLog(LOG_INFO, "Cancel building");
650 }
651
652 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
653 {
654 level->placementPhase = PLACEMENT_PHASE_PLACING;
655 level->placementTimer = 0.0f;
656 }
657 }
658 }
659
660 enum ContextMenuType
661 {
662 CONTEXT_MENU_TYPE_MAIN,
663 CONTEXT_MENU_TYPE_SELL_CONFIRM,
664 CONTEXT_MENU_TYPE_UPGRADE,
665 };
666
667 enum UpgradeType
668 {
669 UPGRADE_TYPE_SPEED,
670 UPGRADE_TYPE_DAMAGE,
671 UPGRADE_TYPE_RANGE,
672 };
673
674 typedef struct ContextMenuArgs
675 {
676 void *data;
677 uint8_t uint8;
678 int32_t int32;
679 Tower *tower;
680 } ContextMenuArgs;
681
682 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
683 {
684 uint8_t towerType = data->uint8;
685 level->placementMode = towerType;
686 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
687 return 1;
688 }
689
690 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
691 {
692 Tower *tower = data->tower;
693 int gold = data->int32;
694 level->playerGold += gold;
695 tower->towerType = TOWER_TYPE_NONE;
696 return 1;
697 }
698
699 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
700 {
701 return 1;
702 }
703
704 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
705 {
706 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
707 return 0;
708 }
709
710 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
711 {
712 Tower *tower = data->tower;
713 switch (data->uint8)
714 {
715 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
716 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
717 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
718 }
719 level->playerGold -= data->int32;
720 return 0;
721 }
722
723 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
724 {
725 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
726 return 0;
727 }
728
729 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
730 {
731 Tower *tower = data->tower;
732 if (level->playerGold >= 1)
733 {
734 level->playerGold -= 1;
735 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
736 }
737 return tower->damage == 0.0f;
738 }
739
740 typedef struct ContextMenuItem
741 {
742 uint8_t index;
743 char text[24];
744 float alignX;
745 int (*action)(Level*, ContextMenuArgs*);
746 void *data;
747 ContextMenuArgs args;
748 ButtonState buttonState;
749 } ContextMenuItem;
750
751 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
752 {
753 ContextMenuItem item = {.index = index, .alignX = alignX};
754 strncpy(item.text, text, 24);
755 return item;
756 }
757
758 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
759 {
760 ContextMenuItem item = {.index = index, .action = action, .args = args};
761 strncpy(item.text, text, 24);
762 return item;
763 }
764
765 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
766 {
767 const int itemHeight = 28;
768 const int itemSpacing = 1;
769 const int padding = 8;
770 int itemCount = 0;
771 for (int i = 0; menus[i].text[0]; i++)
772 {
773 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
774 }
775
776 Rectangle contextMenu = {0, 0, width,
777 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
778
779 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
780 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
781
782 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
783 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
784 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
785 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
786
787 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
788 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
789 const int itemX = contextMenu.x + itemSpacing;
790 const int itemWidth = contextMenu.width - itemSpacing * 2;
791 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
792 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
793 int status = 0;
794 for (int i = 0; menus[i].text[0]; i++)
795 {
796 if (menus[i].action)
797 {
798 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
799 {
800 status = menus[i].action(level, &menus[i].args);
801 }
802 }
803 else
804 {
805 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
806 }
807 }
808
809 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
810 {
811 return 1;
812 }
813
814 return status;
815 }
816
817 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
818 {
819 ContextMenuItem menu[12] = {0};
820 int menuCount = 0;
821 int menuIndex = 0;
822 if (tower)
823 {
824
825 if (tower) {
826 menu[menuCount++] = ContextMenuItemText(menuIndex++, TowerTypeGetName(tower->towerType), 0.5f);
827 }
828
829 // two texts, same line
830 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
831 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
832
833 if (tower->towerType != TOWER_TYPE_BASE)
834 {
835 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
836 (ContextMenuArgs){.tower = tower});
837 }
838
839 if (tower->towerType != TOWER_TYPE_BASE)
840 {
841
842 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
843 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
844 }
845 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
846 {
847 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
848 (ContextMenuArgs){.tower = tower});
849 }
850 }
851 else
852 {
853 menu[menuCount] = ContextMenuItemButton(menuIndex++,
854 TextFormat("Wall: %dG", TowerTypeGetCosts(TOWER_TYPE_WALL)),
855 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
856 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_WALL);
857
858 menu[menuCount] = ContextMenuItemButton(menuIndex++,
859 TextFormat("Archer: %dG", TowerTypeGetCosts(TOWER_TYPE_ARCHER)),
860 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
861 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_ARCHER);
862
863 menu[menuCount] = ContextMenuItemButton(menuIndex++,
864 TextFormat("Ballista: %dG", TowerTypeGetCosts(TOWER_TYPE_BALLISTA)),
865 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
866 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_BALLISTA);
867
868 menu[menuCount] = ContextMenuItemButton(menuIndex++,
869 TextFormat("Catapult: %dG", TowerTypeGetCosts(TOWER_TYPE_CATAPULT)),
870 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
871 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_CATAPULT);
872 }
873
874 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
875 {
876 level->placementContextMenuStatus = -1;
877 }
878 }
879
880 void DrawLevelBuildingState(Level *level)
881 {
882 // when the context menu is not active, we update the placement position
883 if (level->placementContextMenuStatus == 0)
884 {
885 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
886 float hitDistance = ray.position.y / -ray.direction.y;
887 float hitX = ray.direction.x * hitDistance + ray.position.x;
888 float hitY = ray.direction.z * hitDistance + ray.position.z;
889 level->placementX = (int)floorf(hitX + 0.5f);
890 level->placementY = (int)floorf(hitY + 0.5f);
891 }
892
893 // the currently hovered/selected tower
894 Tower *tower = TowerGetAt(level->placementX, level->placementY);
895 // show the range of the tower when hovering/selecting it
896 TowerUpdateAllRangeFade(tower, 0.0f);
897
898 BeginMode3D(level->camera);
899 DrawLevelGround(level);
900 PathFindingMapUpdate(0, 0);
901 TowerDrawAll();
902 EnemyDraw();
903 ProjectileDraw();
904 ParticleDraw();
905 DrawEnemyPaths(level);
906
907 guiState.isBlocked = 0;
908
909 // Hover rectangle, when the mouse is over the map
910 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
911 if (isHovering)
912 {
913 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
914 }
915
916 EndMode3D();
917
918 TowerDrawAllHealthBars(level->camera);
919
920 DrawTitle("Building phase");
921
922 // Draw the context menu when the context menu is active
923 if (level->placementContextMenuStatus >= 1)
924 {
925 float maxHitpoints = 0.0f;
926 float hp = 0.0f;
927 float damageFactor = 0.0f;
928 int32_t sellValue = 0;
929
930 if (tower)
931 {
932 maxHitpoints = TowerGetMaxHealth(tower);
933 hp = maxHitpoints - tower->damage;
934 damageFactor = 1.0f - tower->damage / maxHitpoints;
935 sellValue = (int32_t) ceilf(TowerTypeGetCosts(tower->towerType) * 0.5f * damageFactor);
936 }
937
938 ContextMenuItem menu[12] = {0};
939 int menuCount = 0;
940 int menuIndex = 0;
941 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
942 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
943
944 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
945 {
946 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
947 }
948 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
949 {
950 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
951 int costs = totalLevel * 4;
952 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
953 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
954 TowerTypeGetName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
955 int buttonMenuIndex = menuIndex;
956 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
957 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
958 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
959 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
960 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
961 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
962
963 // check if buttons should be disabled
964 if (isMaxLevel || level->playerGold < costs)
965 {
966 for (int i = buttonMenuIndex; i < menuCount; i++)
967 {
968 menu[i].buttonState.isDisabled = 1;
969 }
970 }
971
972 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
973 {
974 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
975 }
976 }
977 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
978 {
979 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", TowerTypeGetName(tower->towerType)), 0.5f);
980 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
981 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
982 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
983 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
984 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
985 {
986 level->placementContextMenuStatus = -1;
987 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
988 }
989 }
990 }
991
992 // Activate the context menu when the mouse is clicked and the context menu is not active
993 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
994 {
995 level->placementContextMenuStatus += 1;
996 }
997
998 if (level->placementContextMenuStatus == 0)
999 {
1000 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1001 {
1002 level->nextState = LEVEL_STATE_RESET;
1003 }
1004
1005 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1006 {
1007 level->nextState = LEVEL_STATE_BATTLE;
1008 }
1009
1010 }
1011 }
1012
1013 void InitBattleStateConditions(Level *level)
1014 {
1015 level->state = LEVEL_STATE_BATTLE;
1016 level->nextState = LEVEL_STATE_NONE;
1017 level->waveEndTimer = 0.0f;
1018 for (int i = 0; i < 10; i++)
1019 {
1020 EnemyWave *wave = &level->waves[i];
1021 wave->spawned = 0;
1022 wave->timeToSpawnNext = wave->delay;
1023 }
1024 }
1025
1026 void DrawLevelBattleState(Level *level)
1027 {
1028 BeginMode3D(level->camera);
1029 DrawLevelGround(level);
1030 TowerDrawAll();
1031 EnemyDraw();
1032 ProjectileDraw();
1033 ParticleDraw();
1034 guiState.isBlocked = 0;
1035 EndMode3D();
1036
1037 EnemyDrawHealthbars(level->camera);
1038 TowerDrawAllHealthBars(level->camera);
1039
1040 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1041 {
1042 level->nextState = LEVEL_STATE_RESET;
1043 }
1044
1045 int maxCount = 0;
1046 int remainingCount = 0;
1047 for (int i = 0; i < 10; i++)
1048 {
1049 EnemyWave *wave = &level->waves[i];
1050 if (wave->wave != level->currentWave)
1051 {
1052 continue;
1053 }
1054 maxCount += wave->count;
1055 remainingCount += wave->count - wave->spawned;
1056 }
1057 int aliveCount = EnemyCount();
1058 remainingCount += aliveCount;
1059
1060 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1061 DrawTitle(text);
1062 }
1063
1064 void DrawLevel(Level *level)
1065 {
1066 switch (level->state)
1067 {
1068 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1069 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1070 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1071 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1072 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1073 default: break;
1074 }
1075
1076 DrawLevelHud(level);
1077 }
1078
1079 void UpdateLevel(Level *level)
1080 {
1081 if (level->state == LEVEL_STATE_BATTLE)
1082 {
1083 int activeWaves = 0;
1084 for (int i = 0; i < 10; i++)
1085 {
1086 EnemyWave *wave = &level->waves[i];
1087 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1088 {
1089 continue;
1090 }
1091 activeWaves++;
1092 wave->timeToSpawnNext -= gameTime.deltaTime;
1093 if (wave->timeToSpawnNext <= 0.0f)
1094 {
1095 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1096 if (enemy)
1097 {
1098 wave->timeToSpawnNext = wave->interval;
1099 wave->spawned++;
1100 }
1101 }
1102 }
1103 if (TowerGetByType(TOWER_TYPE_BASE) == 0) {
1104 level->waveEndTimer += gameTime.deltaTime;
1105 if (level->waveEndTimer >= 2.0f)
1106 {
1107 level->nextState = LEVEL_STATE_LOST_WAVE;
1108 }
1109 }
1110 else if (activeWaves == 0 && EnemyCount() == 0)
1111 {
1112 level->waveEndTimer += gameTime.deltaTime;
1113 if (level->waveEndTimer >= 2.0f)
1114 {
1115 level->nextState = LEVEL_STATE_WON_WAVE;
1116 }
1117 }
1118 }
1119
1120 PathFindingMapUpdate(0, 0);
1121 EnemyUpdate();
1122 TowerUpdate();
1123 ProjectileUpdate();
1124 ParticleUpdate();
1125
1126 if (level->nextState == LEVEL_STATE_RESET)
1127 {
1128 InitLevel(level);
1129 }
1130
1131 if (level->nextState == LEVEL_STATE_BATTLE)
1132 {
1133 InitBattleStateConditions(level);
1134 }
1135
1136 if (level->nextState == LEVEL_STATE_WON_WAVE)
1137 {
1138 level->currentWave++;
1139 level->state = LEVEL_STATE_WON_WAVE;
1140 }
1141
1142 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1143 {
1144 level->state = LEVEL_STATE_LOST_WAVE;
1145 }
1146
1147 if (level->nextState == LEVEL_STATE_BUILDING)
1148 {
1149 level->state = LEVEL_STATE_BUILDING;
1150 level->placementContextMenuStatus = 0;
1151 }
1152
1153 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1154 {
1155 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1156 level->placementTransitionPosition = (Vector2){
1157 level->placementX, level->placementY};
1158 // initialize the spring to the current position
1159 level->placementTowerSpring = (PhysicsPoint){
1160 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1161 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1162 };
1163 level->placementPhase = PLACEMENT_PHASE_STARTING;
1164 level->placementTimer = 0.0f;
1165 }
1166
1167 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1168 {
1169 // make something of this later
1170 InitLevel(level);
1171 }
1172
1173 level->nextState = LEVEL_STATE_NONE;
1174 }
1175
1176 float nextSpawnTime = 0.0f;
1177
1178 void ResetGame()
1179 {
1180 InitLevel(currentLevel);
1181 }
1182
1183 void InitGame()
1184 {
1185 TowerInit();
1186 EnemyInit();
1187 ProjectileInit();
1188 ParticleInit();
1189 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1190
1191 currentLevel = levels;
1192 InitLevel(currentLevel);
1193 }
1194
1195 //# Immediate GUI functions
1196
1197 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1198 {
1199 const float healthBarHeight = 6.0f;
1200 const float healthBarOffset = 15.0f;
1201 const float inset = 2.0f;
1202 const float innerWidth = healthBarWidth - inset * 2;
1203 const float innerHeight = healthBarHeight - inset * 2;
1204
1205 Vector2 screenPos = GetWorldToScreen(position, camera);
1206 screenPos = Vector2Add(screenPos, screenOffset);
1207 float centerX = screenPos.x - healthBarWidth * 0.5f;
1208 float topY = screenPos.y - healthBarOffset;
1209 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1210 float healthWidth = innerWidth * healthRatio;
1211 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1212 }
1213
1214 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1215 {
1216 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1217
1218 DrawTextEx(gameFontNormal, text, (Vector2){
1219 x + (width - textSize.x) * alignX,
1220 y + (height - textSize.y) * alignY
1221 }, gameFontNormal.baseSize, 1, textColor);
1222 }
1223
1224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1225 {
1226 Rectangle bounds = {x, y, width, height};
1227 int isPressed = 0;
1228 int isSelected = state && state->isSelected;
1229 int isDisabled = state && state->isDisabled;
1230 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1231 {
1232 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1233 {
1234 isPressed = 1;
1235 }
1236 guiState.isBlocked = 1;
1237 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1238 bounds, Vector2Zero(), 0, WHITE);
1239 }
1240 else
1241 {
1242 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1243 bounds, Vector2Zero(), 0, WHITE);
1244 }
1245 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1246 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1247 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1248 return isPressed;
1249 }
1250
1251 //# Main game loop
1252
1253 void GameUpdate()
1254 {
1255 UpdateLevel(currentLevel);
1256 }
1257
1258 int main(void)
1259 {
1260 int screenWidth, screenHeight;
1261 GetPreferredSize(&screenWidth, &screenHeight);
1262 InitWindow(screenWidth, screenHeight, "Tower defense");
1263 float gamespeed = 1.0f;
1264 int frameRate = 30;
1265 SetTargetFPS(30);
1266
1267 LoadAssets();
1268 InitGame();
1269
1270 float pause = 1.0f;
1271
1272 while (!WindowShouldClose())
1273 {
1274 if (IsPaused()) {
1275 // canvas is not visible in browser - do nothing
1276 continue;
1277 }
1278
1279 if (IsKeyPressed(KEY_F))
1280 {
1281 frameRate = (frameRate + 5) % 30;
1282 frameRate = frameRate < 10 ? 10 : frameRate;
1283 SetTargetFPS(frameRate);
1284 }
1285
1286 if (IsKeyPressed(KEY_T))
1287 {
1288 gamespeed += 0.1f;
1289 if (gamespeed > 1.05f) gamespeed = 0.1f;
1290 }
1291
1292 if (IsKeyPressed(KEY_P))
1293 {
1294 pause = pause > 0.5f ? 0.0f : 1.0f;
1295 }
1296
1297 float dt = GetFrameTime() * gamespeed * pause;
1298 // cap maximum delta time to 0.1 seconds to prevent large time steps
1299 if (dt > 0.1f) dt = 0.1f;
1300 gameTime.time += dt;
1301 gameTime.deltaTime = dt;
1302 gameTime.frameCount += 1;
1303
1304 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1305 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1306
1307 BeginDrawing();
1308 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1309
1310 GameUpdate();
1311 DrawLevel(currentLevel);
1312
1313 if (gamespeed != 1.0f)
1314 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1315 EndDrawing();
1316
1317 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1318 }
1319
1320 CloseWindow();
1321
1322 return 0;
1323 }
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 maxUpgradeRange;
65 float projectileSpeed;
66
67 uint8_t cost;
68 uint8_t projectileType;
69 uint16_t maxHealth;
70
71 HitEffectConfig hitEffect;
72 } TowerTypeConfig;
73
74 #define TOWER_MAX_STAGE 10
75
76 typedef struct TowerUpgradeState
77 {
78 uint8_t range;
79 uint8_t damage;
80 uint8_t speed;
81 } TowerUpgradeState;
82
83 typedef struct Tower
84 {
85 int16_t x, y;
86 uint8_t towerType;
87 TowerUpgradeState upgradeState;
88 Vector2 lastTargetPosition;
89 float cooldown;
90 float damage;
91 // alpha value for the range circle drawing
92 float drawRangeAlpha;
93 } Tower;
94
95 typedef struct GameTime
96 {
97 float time;
98 float deltaTime;
99 uint32_t frameCount;
100
101 float fixedDeltaTime;
102 // leaving the fixed time stepping to the update functions,
103 // we need to know the fixed time at the start of the frame
104 float fixedTimeStart;
105 // and the number of fixed steps that we have to make this frame
106 // The fixedTime is fixedTimeStart + n * fixedStepCount
107 uint8_t fixedStepCount;
108 } GameTime;
109
110 typedef struct ButtonState {
111 char isSelected;
112 char isDisabled;
113 } ButtonState;
114
115 typedef struct GUIState {
116 int isBlocked;
117 } GUIState;
118
119 typedef enum LevelState
120 {
121 LEVEL_STATE_NONE,
122 LEVEL_STATE_BUILDING,
123 LEVEL_STATE_BUILDING_PLACEMENT,
124 LEVEL_STATE_BATTLE,
125 LEVEL_STATE_WON_WAVE,
126 LEVEL_STATE_LOST_WAVE,
127 LEVEL_STATE_WON_LEVEL,
128 LEVEL_STATE_RESET,
129 } LevelState;
130
131 typedef struct EnemyWave {
132 uint8_t enemyType;
133 uint8_t wave;
134 uint16_t count;
135 float interval;
136 float delay;
137 Vector2 spawnPosition;
138
139 uint16_t spawned;
140 float timeToSpawnNext;
141 } EnemyWave;
142
143 #define ENEMY_MAX_WAVE_COUNT 10
144
145 typedef enum PlacementPhase
146 {
147 PLACEMENT_PHASE_STARTING,
148 PLACEMENT_PHASE_MOVING,
149 PLACEMENT_PHASE_PLACING,
150 } PlacementPhase;
151
152 typedef struct Level
153 {
154 int seed;
155 LevelState state;
156 LevelState nextState;
157 Camera3D camera;
158 int placementMode;
159 PlacementPhase placementPhase;
160 float placementTimer;
161
162 int16_t placementX;
163 int16_t placementY;
164 int8_t placementContextMenuStatus;
165 int8_t placementContextMenuType;
166
167 Vector2 placementTransitionPosition;
168 PhysicsPoint placementTowerSpring;
169
170 int initialGold;
171 int playerGold;
172
173 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
174 int currentWave;
175 float waveEndTimer;
176 } Level;
177
178 typedef struct DeltaSrc
179 {
180 char x, y;
181 } DeltaSrc;
182
183 typedef struct PathfindingMap
184 {
185 int width, height;
186 float scale;
187 float *distances;
188 long *towerIndex;
189 DeltaSrc *deltaSrc;
190 float maxDistance;
191 Matrix toMapSpace;
192 Matrix toWorldSpace;
193 } PathfindingMap;
194
195 // when we execute the pathfinding algorithm, we need to store the active nodes
196 // in a queue. Each node has a position, a distance from the start, and the
197 // position of the node that we came from.
198 typedef struct PathfindingNode
199 {
200 int16_t x, y, fromX, fromY;
201 float distance;
202 } PathfindingNode;
203
204 typedef struct EnemyId
205 {
206 uint16_t index;
207 uint16_t generation;
208 } EnemyId;
209
210 typedef struct EnemyClassConfig
211 {
212 float speed;
213 float health;
214 float shieldHealth;
215 float shieldDamageAbsorption;
216 float radius;
217 float maxAcceleration;
218 float requiredContactTime;
219 float explosionDamage;
220 float explosionRange;
221 float explosionPushbackPower;
222 int goldValue;
223 } EnemyClassConfig;
224
225 typedef struct Enemy
226 {
227 int16_t currentX, currentY;
228 int16_t nextX, nextY;
229 Vector2 simPosition;
230 Vector2 simVelocity;
231 uint16_t generation;
232 float walkedDistance;
233 float startMovingTime;
234 float damage, futureDamage;
235 float shieldDamage;
236 float contactTime;
237 uint8_t enemyType;
238 uint8_t movePathCount;
239 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
240 } Enemy;
241
242 // a unit that uses sprites to be drawn
243 #define SPRITE_UNIT_ANIMATION_COUNT 6
244 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
245 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
246 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
247
248 typedef struct SpriteAnimation
249 {
250 Rectangle srcRect;
251 Vector2 offset;
252 uint8_t animationId;
253 uint8_t frameCount;
254 uint8_t frameWidth;
255 float frameDuration;
256 } SpriteAnimation;
257
258 typedef struct SpriteUnit
259 {
260 float scale;
261 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
262 } SpriteUnit;
263
264 #define PROJECTILE_MAX_COUNT 1200
265 #define PROJECTILE_TYPE_NONE 0
266 #define PROJECTILE_TYPE_ARROW 1
267 #define PROJECTILE_TYPE_CATAPULT 2
268 #define PROJECTILE_TYPE_BALLISTA 3
269
270 typedef struct Projectile
271 {
272 uint8_t projectileType;
273 float shootTime;
274 float arrivalTime;
275 float distance;
276 Vector3 position;
277 Vector3 target;
278 Vector3 directionNormal;
279 EnemyId targetEnemy;
280 HitEffectConfig hitEffectConfig;
281 } Projectile;
282
283 //# Function declarations
284 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
285 int EnemyAddDamageRange(Vector2 position, float range, float damage);
286 int EnemyAddDamage(Enemy *enemy, float damage);
287
288 //# Enemy functions
289 void EnemyInit();
290 void EnemyDraw();
291 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
292 void EnemyUpdate();
293 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
294 float EnemyGetMaxHealth(Enemy *enemy);
295 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
296 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
297 EnemyId EnemyGetId(Enemy *enemy);
298 Enemy *EnemyTryResolve(EnemyId enemyId);
299 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
300 int EnemyAddDamage(Enemy *enemy, float damage);
301 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
302 int EnemyCount();
303 void EnemyDrawHealthbars(Camera3D camera);
304
305 //# Tower functions
306 const char *TowerTypeGetName(uint8_t towerType);
307 int TowerTypeGetCosts(uint8_t towerType);
308 void TowerInit();
309 void TowerUpdate();
310 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget);
311 void TowerDrawAll();
312 void TowerDrawAllHealthBars(Camera3D camera);
313 void TowerDrawModel(Tower *tower);
314 void TowerDrawRange(Tower *tower, float alpha);
315 Tower *TowerGetByIndex(int index);
316 Tower *TowerGetByType(uint8_t towerType);
317 Tower *TowerGetAt(int16_t x, int16_t y);
318 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
319 float TowerGetMaxHealth(Tower *tower);
320 float TowerGetRange(Tower *tower);
321
322 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
323
324 //# Particles
325 void ParticleInit();
326 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
327 void ParticleUpdate();
328 void ParticleDraw();
329
330 //# Projectiles
331 void ProjectileInit();
332 void ProjectileDraw();
333 void ProjectileUpdate();
334 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
335
336 //# Pathfinding map
337 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
338 float PathFindingGetDistance(int mapX, int mapY);
339 Vector2 PathFindingGetGradient(Vector3 world);
340 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
341 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
342 void PathFindingMapDraw();
343
344 //# UI
345 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
346
347 //# Level
348 void DrawLevelGround(Level *level);
349 void DrawEnemyPath(Level *level, Color arrowColor);
350
351 //# variables
352 extern Level *currentLevel;
353 extern Enemy enemies[ENEMY_MAX_COUNT];
354 extern int enemyCount;
355 extern EnemyClassConfig enemyClassConfigs[];
356
357 extern GUIState guiState;
358 extern GameTime gameTime;
359 extern Tower towers[TOWER_MAX_COUNT];
360 extern int towerCount;
361
362 extern Texture2D palette, spriteSheet;
363
364 #endif
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 .maxUpgradeRange = 5.0f,
14 .cost = 6,
15 .maxHealth = 10,
16 .projectileSpeed = 4.0f,
17 .projectileType = PROJECTILE_TYPE_ARROW,
18 .hitEffect = {
19 .damage = 3.0f,
20 }
21 },
22 [TOWER_TYPE_BALLISTA] = {
23 .name = "Ballista",
24 .cooldown = 1.5f,
25 .range = 6.0f,
26 .maxUpgradeRange = 8.0f,
27 .cost = 9,
28 .maxHealth = 10,
29 .projectileSpeed = 10.0f,
30 .projectileType = PROJECTILE_TYPE_BALLISTA,
31 .hitEffect = {
32 .damage = 8.0f,
33 .pushbackPowerDistance = 0.25f,
34 }
35 },
36 [TOWER_TYPE_CATAPULT] = {
37 .name = "Catapult",
38 .cooldown = 1.7f,
39 .range = 5.0f,
40 .maxUpgradeRange = 7.0f,
41 .cost = 10,
42 .maxHealth = 10,
43 .projectileSpeed = 3.0f,
44 .projectileType = PROJECTILE_TYPE_CATAPULT,
45 .hitEffect = {
46 .damage = 2.0f,
47 .areaDamageRadius = 1.75f,
48 }
49 },
50 [TOWER_TYPE_WALL] = {
51 .name = "Wall",
52 .cost = 2,
53 .maxHealth = 10,
54 },
55 };
56
57 Tower towers[TOWER_MAX_COUNT];
58 int towerCount = 0;
59
60 Model towerModels[TOWER_TYPE_COUNT];
61
62 // definition of our archer unit
63 SpriteUnit archerUnit = {
64 .animations[0] = {
65 .srcRect = {0, 0, 16, 16},
66 .offset = {7, 1},
67 .frameCount = 1,
68 .frameDuration = 0.0f,
69 },
70 .animations[1] = {
71 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
72 .srcRect = {16, 0, 6, 16},
73 .offset = {8, 0},
74 },
75 .animations[2] = {
76 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
77 .srcRect = {22, 0, 11, 16},
78 .offset = {10, 0},
79 },
80 };
81
82 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
83 {
84 float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
85 float xScale = flip ? -1.0f : 1.0f;
86 Camera3D camera = currentLevel->camera;
87 float size = 0.5f * unitScale;
88 // we want the sprite to face the camera, so we need to calculate the up vector
89 Vector3 forward = Vector3Subtract(camera.target, camera.position);
90 Vector3 up = {0, 1, 0};
91 Vector3 right = Vector3CrossProduct(forward, up);
92 up = Vector3Normalize(Vector3CrossProduct(right, forward));
93
94 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
95 {
96 SpriteAnimation anim = unit.animations[i];
97 if (anim.animationId != phase && anim.animationId != 0)
98 {
99 continue;
100 }
101 Rectangle srcRect = anim.srcRect;
102 if (anim.frameCount > 1)
103 {
104 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
105 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
106 }
107 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
108 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
109
110 if (flip)
111 {
112 srcRect.x += srcRect.width;
113 srcRect.width = -srcRect.width;
114 offset.x = scale.x - offset.x;
115 }
116 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
117 // move the sprite slightly towards the camera to avoid z-fighting
118 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
119 }
120 }
121
122 void TowerInit()
123 {
124 for (int i = 0; i < TOWER_MAX_COUNT; i++)
125 {
126 towers[i] = (Tower){0};
127 }
128 towerCount = 0;
129
130 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
131 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
132
133 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
134 {
135 if (towerModels[i].materials)
136 {
137 // assign the palette texture to the material of the model (0 is not used afaik)
138 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
139 }
140 }
141 }
142
143 static void TowerGunUpdate(Tower *tower)
144 {
145 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
146 if (tower->cooldown <= 0.0f)
147 {
148 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, TowerGetRange(tower));
149 if (enemy)
150 {
151 tower->cooldown = config.cooldown;
152 // shoot the enemy; determine future position of the enemy
153 float bulletSpeed = config.projectileSpeed;
154 Vector2 velocity = enemy->simVelocity;
155 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
156 Vector2 towerPosition = {tower->x, tower->y};
157 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
158 for (int i = 0; i < 8; i++) {
159 velocity = enemy->simVelocity;
160 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
161 float distance = Vector2Distance(towerPosition, futurePosition);
162 float eta2 = distance / bulletSpeed;
163 if (fabs(eta - eta2) < 0.01f) {
164 break;
165 }
166 eta = (eta2 + eta) * 0.5f;
167 }
168
169 ProjectileTryAdd(config.projectileType, enemy,
170 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
171 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
172 bulletSpeed, config.hitEffect);
173 enemy->futureDamage += config.hitEffect.damage;
174 tower->lastTargetPosition = futurePosition;
175 }
176 }
177 else
178 {
179 tower->cooldown -= gameTime.deltaTime;
180 }
181 }
182
183 Tower *TowerGetAt(int16_t x, int16_t y)
184 {
185 for (int i = 0; i < towerCount; i++)
186 {
187 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
188 {
189 return &towers[i];
190 }
191 }
192 return 0;
193 }
194
195 Tower *TowerGetByIndex(int index)
196 {
197 if (index < 0 || index >= towerCount)
198 {
199 return 0;
200 }
201 return &towers[index];
202 }
203
204 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
205 {
206 if (towerCount >= TOWER_MAX_COUNT)
207 {
208 return 0;
209 }
210
211 Tower *tower = TowerGetAt(x, y);
212 if (tower)
213 {
214 return 0;
215 }
216
217 tower = &towers[towerCount++];
218 *tower = (Tower){
219 .x = x,
220 .y = y,
221 .towerType = towerType,
222 .cooldown = 0.0f,
223 .damage = 0.0f,
224 };
225 return tower;
226 }
227
228 Tower *TowerGetByType(uint8_t towerType)
229 {
230 for (int i = 0; i < towerCount; i++)
231 {
232 if (towers[i].towerType == towerType)
233 {
234 return &towers[i];
235 }
236 }
237 return 0;
238 }
239
240 const char *TowerTypeGetName(uint8_t towerType)
241 {
242 return towerTypeConfigs[towerType].name;
243 }
244
245 int TowerTypeGetCosts(uint8_t towerType)
246 {
247 return towerTypeConfigs[towerType].cost;
248 }
249
250 float TowerGetMaxHealth(Tower *tower)
251 {
252 return towerTypeConfigs[tower->towerType].maxHealth;
253 }
254
255 float TowerGetRange(Tower *tower)
256 {
257 float range = towerTypeConfigs[tower->towerType].range;
258 float maxUpgradeRange = towerTypeConfigs[tower->towerType].maxUpgradeRange;
259 if (tower->upgradeState.range > 0)
260 {
261 range = Lerp(range, maxUpgradeRange, tower->upgradeState.range / (float)TOWER_MAX_STAGE);
262 }
263 return range;
264 }
265
266 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget)
267 {
268 // animate fade in and fade out of range drawing using framerate independent lerp
269 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime);
270 for (int i = 0; i < TOWER_MAX_COUNT; i++)
271 {
272 Tower *fadingTower = TowerGetByIndex(i);
273 if (!fadingTower)
274 {
275 break;
276 }
277 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget;
278 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp);
279 }
280 }
281
282 void TowerDrawRange(Tower *tower, float alpha)
283 {
284 Color ringColor = (Color){255, 200, 100, 255};
285 const int rings = 4;
286 const float radiusOffset = 0.5f;
287 const float animationSpeed = 2.0f;
288 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
289 float radius = TowerGetRange(tower);
290 // base circle
291 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, radius, (Vector3){1, 0, 0}, 90,
292 Fade(ringColor, alpha));
293
294 for (int i = 1; i < rings; i++)
295 {
296 float t = ((float)i + animation) / (float)rings;
297 float r = Lerp(radius, radius - radiusOffset, t * t);
298 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
299 if (i == 1)
300 {
301 // fade out the outermost ring
302 a = animation;
303 }
304 a *= alpha;
305
306 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, r, (Vector3){1, 0, 0}, 90,
307 Fade(ringColor, a));
308 }
309 }
310
311 void TowerDrawModel(Tower *tower)
312 {
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 return;
316 }
317
318 if (tower->drawRangeAlpha > 2.0f/256.0f)
319 {
320 TowerDrawRange(tower, tower->drawRangeAlpha);
321 }
322
323 switch (tower->towerType)
324 {
325 case TOWER_TYPE_ARCHER:
326 {
327 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower->x, 0.0f, tower->y}, currentLevel->camera);
328 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower->lastTargetPosition.x, 0.0f, tower->lastTargetPosition.y}, currentLevel->camera);
329 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
330 DrawSpriteUnit(archerUnit, (Vector3){tower->x, 1.0f, tower->y}, 0, screenPosTarget.x > screenPosTower.x,
331 tower->cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
332 }
333 break;
334 case TOWER_TYPE_BALLISTA:
335 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, BROWN);
336 break;
337 case TOWER_TYPE_CATAPULT:
338 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
339 break;
340 default:
341 if (towerModels[tower->towerType].materials)
342 {
343 DrawModel(towerModels[tower->towerType], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
344 } else {
345 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
346 }
347 break;
348 }
349 }
350
351 void TowerDrawAll()
352 {
353 for (int i = 0; i < towerCount; i++)
354 {
355 TowerDrawModel(&towers[i]);
356 }
357 }
358
359 void TowerUpdate()
360 {
361 for (int i = 0; i < towerCount; i++)
362 {
363 Tower *tower = &towers[i];
364 switch (tower->towerType)
365 {
366 case TOWER_TYPE_CATAPULT:
367 case TOWER_TYPE_BALLISTA:
368 case TOWER_TYPE_ARCHER:
369 TowerGunUpdate(tower);
370 break;
371 }
372 }
373 }
374
375 void TowerDrawAllHealthBars(Camera3D camera)
376 {
377 for (int i = 0; i < towerCount; i++)
378 {
379 Tower *tower = &towers[i];
380 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
381 {
382 continue;
383 }
384
385 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
386 float maxHealth = TowerGetMaxHealth(tower);
387 float health = maxHealth - tower->damage;
388 float healthRatio = health / maxHealth;
389
390 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
391 }
392 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 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
Now that the cleanups are done, we can add the speed and damage upgrades. To test the upgrades, we bump the amount of money the player has to 500. This way we can easily test the upgrades.
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 = 500,
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 TowerUpdateAllRangeFade(0, 0.0f);
205 TowerDrawAll();
206 EnemyDraw();
207 ProjectileDraw();
208 ParticleDraw();
209 guiState.isBlocked = 0;
210 EndMode3D();
211
212 TowerDrawAllHealthBars(level->camera);
213
214 DrawTitle("Wave lost");
215
216 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
217 {
218 level->nextState = LEVEL_STATE_RESET;
219 }
220 }
221
222 int HasLevelNextWave(Level *level)
223 {
224 for (int i = 0; i < 10; i++)
225 {
226 EnemyWave *wave = &level->waves[i];
227 if (wave->wave == level->currentWave)
228 {
229 return 1;
230 }
231 }
232 return 0;
233 }
234
235 void DrawLevelReportWonWave(Level *level)
236 {
237 BeginMode3D(level->camera);
238 DrawLevelGround(level);
239 TowerUpdateAllRangeFade(0, 0.0f);
240 TowerDrawAll();
241 EnemyDraw();
242 ProjectileDraw();
243 ParticleDraw();
244 guiState.isBlocked = 0;
245 EndMode3D();
246
247 TowerDrawAllHealthBars(level->camera);
248
249 DrawTitle("Wave won");
250
251
252 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
253 {
254 level->nextState = LEVEL_STATE_RESET;
255 }
256
257 if (HasLevelNextWave(level))
258 {
259 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
260 {
261 level->nextState = LEVEL_STATE_BUILDING;
262 }
263 }
264 else {
265 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
266 {
267 level->nextState = LEVEL_STATE_WON_LEVEL;
268 }
269 }
270 }
271
272 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
273 {
274 static ButtonState buttonStates[8] = {0};
275 int cost = TowerTypeGetCosts(towerType);
276 const char *text = TextFormat("%s: %d", name, cost);
277 buttonStates[towerType].isSelected = level->placementMode == towerType;
278 buttonStates[towerType].isDisabled = level->playerGold < cost;
279 if (Button(text, x, y, width, height, &buttonStates[towerType]))
280 {
281 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
282 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
283 return 1;
284 }
285 return 0;
286 }
287
288 float GetRandomFloat(float min, float max)
289 {
290 int random = GetRandomValue(0, 0xfffffff);
291 return ((float)random / (float)0xfffffff) * (max - min) + min;
292 }
293
294 void DrawLevelGround(Level *level)
295 {
296 // draw checkerboard ground pattern
297 for (int x = -5; x <= 5; x += 1)
298 {
299 for (int y = -5; y <= 5; y += 1)
300 {
301 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
302 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
303 }
304 }
305
306 int oldSeed = GetRandomValue(0, 0xfffffff);
307 SetRandomSeed(level->seed);
308 // increase probability for trees via duplicated entries
309 Model borderModels[64];
310 int maxRockCount = GetRandomValue(2, 6);
311 int maxTreeCount = GetRandomValue(10, 20);
312 int maxFirTreeCount = GetRandomValue(5, 10);
313 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
314 int grassPatchCount = GetRandomValue(5, 30);
315
316 int modelCount = 0;
317 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
318 {
319 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
320 }
321 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
322 {
323 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
324 }
325 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
326 {
327 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
328 }
329 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
330 {
331 borderModels[modelCount++] = grassPatchModel[0];
332 }
333
334 // draw some objects around the border of the map
335 Vector3 up = {0, 1, 0};
336 // a pseudo random number generator to get the same result every time
337 const float wiggle = 0.75f;
338 const int layerCount = 3;
339 for (int layer = 0; layer <= layerCount; layer++)
340 {
341 int layerPos = 6 + layer;
342 Model *selectedModels = borderModels;
343 int selectedModelCount = modelCount;
344 if (layer == 0)
345 {
346 selectedModels = grassPatchModel;
347 selectedModelCount = 1;
348 }
349 for (int x = -6 - layer; x <= 6 + layer; x += 1)
350 {
351 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
352 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
353 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
354 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
355 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
356 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
357 }
358
359 for (int z = -5 - layer; z <= 5 + layer; z += 1)
360 {
361 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
362 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
363 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
364 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
365 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
366 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
367 }
368 }
369
370 SetRandomSeed(oldSeed);
371 }
372
373 void DrawEnemyPath(Level *level, Color arrowColor)
374 {
375 const int castleX = 0, castleY = 0;
376 const int maxWaypointCount = 200;
377 const float timeStep = 1.0f;
378 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
379
380 // we start with a time offset to simulate the path,
381 // this way the arrows are animated in a forward moving direction
382 // The time is wrapped around the time step to get a smooth animation
383 float timeOffset = fmodf(GetTime(), timeStep);
384
385 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
386 {
387 EnemyWave *wave = &level->waves[i];
388 if (wave->wave != level->currentWave)
389 {
390 continue;
391 }
392
393 // use this dummy enemy to simulate the path
394 Enemy dummy = {
395 .enemyType = ENEMY_TYPE_MINION,
396 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
397 .nextX = wave->spawnPosition.x,
398 .nextY = wave->spawnPosition.y,
399 .currentX = wave->spawnPosition.x,
400 .currentY = wave->spawnPosition.y,
401 };
402
403 float deltaTime = timeOffset;
404 for (int j = 0; j < maxWaypointCount; j++)
405 {
406 int waypointPassedCount = 0;
407 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
408 // after the initial variable starting offset, we use a fixed time step
409 deltaTime = timeStep;
410 dummy.simPosition = pos;
411
412 // Update the dummy's position just like we do in the regular enemy update loop
413 for (int k = 0; k < waypointPassedCount; k++)
414 {
415 dummy.currentX = dummy.nextX;
416 dummy.currentY = dummy.nextY;
417 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
418 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
419 {
420 break;
421 }
422 }
423 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
424 {
425 break;
426 }
427
428 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
429 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
430 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
431 }
432 }
433 }
434
435 void DrawEnemyPaths(Level *level)
436 {
437 // disable depth testing for the path arrows
438 // flush the 3D batch to draw the arrows on top of everything
439 rlDrawRenderBatchActive();
440 rlDisableDepthTest();
441 DrawEnemyPath(level, (Color){64, 64, 64, 160});
442
443 rlDrawRenderBatchActive();
444 rlEnableDepthTest();
445 DrawEnemyPath(level, WHITE);
446 }
447
448 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
449 {
450 float dt = gameTime.fixedDeltaTime;
451 // smooth transition for the placement position using exponential decay
452 const float lambda = 15.0f;
453 float factor = 1.0f - expf(-lambda * dt);
454
455 float damping = 0.5f;
456 float springStiffness = 300.0f;
457 float springDecay = 95.0f;
458 float minHeight = 0.35f;
459
460 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
461 {
462 damping = 1.0f;
463 springDecay = 90.0f;
464 springStiffness = 100.0f;
465 minHeight = 0.70f;
466 }
467
468 for (int i = 0; i < gameTime.fixedStepCount; i++)
469 {
470 level->placementTransitionPosition =
471 Vector2Lerp(
472 level->placementTransitionPosition,
473 (Vector2){mapX, mapY}, factor);
474
475 // draw the spring position for debugging the spring simulation
476 // first step: stiff spring, no simulation
477 Vector3 worldPlacementPosition = (Vector3){
478 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
479 Vector3 springTargetPosition = (Vector3){
480 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
481 // consider the current velocity to predict the future position in order to dampen
482 // the spring simulation. Longer prediction times will result in more damping
483 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
484 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
485 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
486 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
487 // decay velocity of the upright forcing spring
488 // This force acts like a 2nd spring that pulls the tip upright into the air above the
489 // base position
490 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
491 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
492
493 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
494 // we use a simple spring model with a rest length of 1.0f
495 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
496 float springLength = Vector3Length(springDelta);
497 float springForce = (springLength - 1.0f) * springStiffness;
498 Vector3 springForceVector = Vector3Normalize(springDelta);
499 springForceVector = Vector3Scale(springForceVector, springForce);
500 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
501 Vector3Scale(springForceVector, dt));
502
503 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
504 Vector3Scale(level->placementTowerSpring.velocity, dt));
505 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
506 {
507 level->placementTowerSpring.velocity.y *= -1.0f;
508 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
509 }
510 }
511 }
512
513 void DrawLevelBuildingPlacementState(Level *level)
514 {
515 const float placementDuration = 0.5f;
516
517 level->placementTimer += gameTime.deltaTime;
518 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
519 {
520 level->placementPhase = PLACEMENT_PHASE_MOVING;
521 level->placementTimer = 0.0f;
522 }
523
524 BeginMode3D(level->camera);
525 DrawLevelGround(level);
526
527 int blockedCellCount = 0;
528 Vector2 blockedCells[1];
529 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
530 float planeDistance = ray.position.y / -ray.direction.y;
531 float planeX = ray.direction.x * planeDistance + ray.position.x;
532 float planeY = ray.direction.z * planeDistance + ray.position.z;
533 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
534 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
535 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
536 level->placementMode && !guiState.isBlocked &&
537 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
538 {
539 level->placementX = mapX;
540 level->placementY = mapY;
541 }
542 else
543 {
544 mapX = level->placementX;
545 mapY = level->placementY;
546 }
547 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
548 PathFindingMapUpdate(blockedCellCount, blockedCells);
549
550 TowerUpdateAllRangeFade(0, 0.0f);
551 TowerDrawAll();
552 EnemyDraw();
553 ProjectileDraw();
554 ParticleDraw();
555 DrawEnemyPaths(level);
556
557 // let the tower float up and down. Consider this height in the spring simulation as well
558 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
559
560 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
561 {
562 // The bouncing spring needs a bit of outro time to look nice and complete.
563 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
564 // and the last 1/3rd is the outro physics (bouncing)
565 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
566 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
567 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
568 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
569 towerFloatHeight = linearBlendHeight + parabola;
570 }
571
572 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
573
574 rlPushMatrix();
575 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
576
577 // calculate x and z rotation to align the model with the spring
578 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
579 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
580 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
581 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
582 float springLength = Vector3Length(towerUp);
583 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
584 float towerSquash = 1.0f / towerStretch;
585
586 Tower dummy = {
587 .towerType = level->placementMode,
588 };
589
590 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
591 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
592 {
593 rangeAlpha = 1.0f - rangeAlpha;
594 }
595 else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
596 {
597 rangeAlpha = 1.0f;
598 }
599
600 TowerDrawRange(&dummy, rangeAlpha);
601
602 rlPushMatrix();
603 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
604
605 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
606 rlScalef(towerSquash, towerStretch, towerSquash);
607 TowerDrawModel(&dummy);
608 rlPopMatrix();
609
610
611 // draw a shadow for the tower
612 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
613 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
614 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
615
616
617 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
618 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
619 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
620 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
621
622 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
623 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
624 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
625 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
626 rlPopMatrix();
627
628 guiState.isBlocked = 0;
629
630 EndMode3D();
631
632 TowerDrawAllHealthBars(level->camera);
633
634 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
635 {
636 if (level->placementTimer > placementDuration)
637 {
638 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
639 // testing repairing
640 tower->damage = 2.5f;
641 level->playerGold -= TowerTypeGetCosts(level->placementMode);
642 level->nextState = LEVEL_STATE_BUILDING;
643 level->placementMode = TOWER_TYPE_NONE;
644 }
645 }
646 else
647 {
648 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
649 {
650 level->nextState = LEVEL_STATE_BUILDING;
651 level->placementMode = TOWER_TYPE_NONE;
652 TraceLog(LOG_INFO, "Cancel building");
653 }
654
655 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
656 {
657 level->placementPhase = PLACEMENT_PHASE_PLACING;
658 level->placementTimer = 0.0f;
659 }
660 }
661 }
662
663 enum ContextMenuType
664 {
665 CONTEXT_MENU_TYPE_MAIN,
666 CONTEXT_MENU_TYPE_SELL_CONFIRM,
667 CONTEXT_MENU_TYPE_UPGRADE,
668 };
669
670 enum UpgradeType
671 {
672 UPGRADE_TYPE_SPEED,
673 UPGRADE_TYPE_DAMAGE,
674 UPGRADE_TYPE_RANGE,
675 };
676
677 typedef struct ContextMenuArgs
678 {
679 void *data;
680 uint8_t uint8;
681 int32_t int32;
682 Tower *tower;
683 } ContextMenuArgs;
684
685 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
686 {
687 uint8_t towerType = data->uint8;
688 level->placementMode = towerType;
689 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
690 return 1;
691 }
692
693 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
694 {
695 Tower *tower = data->tower;
696 int gold = data->int32;
697 level->playerGold += gold;
698 tower->towerType = TOWER_TYPE_NONE;
699 return 1;
700 }
701
702 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
703 {
704 return 1;
705 }
706
707 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
708 {
709 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
710 return 0;
711 }
712
713 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
714 {
715 Tower *tower = data->tower;
716 switch (data->uint8)
717 {
718 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
719 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
720 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
721 }
722 level->playerGold -= data->int32;
723 return 0;
724 }
725
726 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
727 {
728 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
729 return 0;
730 }
731
732 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
733 {
734 Tower *tower = data->tower;
735 if (level->playerGold >= 1)
736 {
737 level->playerGold -= 1;
738 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
739 }
740 return tower->damage == 0.0f;
741 }
742
743 typedef struct ContextMenuItem
744 {
745 uint8_t index;
746 char text[24];
747 float alignX;
748 int (*action)(Level*, ContextMenuArgs*);
749 void *data;
750 ContextMenuArgs args;
751 ButtonState buttonState;
752 } ContextMenuItem;
753
754 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
755 {
756 ContextMenuItem item = {.index = index, .alignX = alignX};
757 strncpy(item.text, text, 24);
758 return item;
759 }
760
761 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
762 {
763 ContextMenuItem item = {.index = index, .action = action, .args = args};
764 strncpy(item.text, text, 24);
765 return item;
766 }
767
768 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
769 {
770 const int itemHeight = 28;
771 const int itemSpacing = 1;
772 const int padding = 8;
773 int itemCount = 0;
774 for (int i = 0; menus[i].text[0]; i++)
775 {
776 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
777 }
778
779 Rectangle contextMenu = {0, 0, width,
780 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
781
782 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
783 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
784
785 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
786 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
787 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
788 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
789
790 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
791 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
792 const int itemX = contextMenu.x + itemSpacing;
793 const int itemWidth = contextMenu.width - itemSpacing * 2;
794 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
795 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
796 int status = 0;
797 for (int i = 0; menus[i].text[0]; i++)
798 {
799 if (menus[i].action)
800 {
801 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
802 {
803 status = menus[i].action(level, &menus[i].args);
804 }
805 }
806 else
807 {
808 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
809 }
810 }
811
812 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
813 {
814 return 1;
815 }
816
817 return status;
818 }
819
820 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
821 {
822 ContextMenuItem menu[12] = {0};
823 int menuCount = 0;
824 int menuIndex = 0;
825 if (tower)
826 {
827
828 if (tower) {
829 menu[menuCount++] = ContextMenuItemText(menuIndex++, TowerTypeGetName(tower->towerType), 0.5f);
830 }
831
832 // two texts, same line
833 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
834 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
835
836 if (tower->towerType != TOWER_TYPE_BASE)
837 {
838 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
839 (ContextMenuArgs){.tower = tower});
840 }
841
842 if (tower->towerType != TOWER_TYPE_BASE)
843 {
844
845 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
846 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
847 }
848 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
849 {
850 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
851 (ContextMenuArgs){.tower = tower});
852 }
853 }
854 else
855 {
856 menu[menuCount] = ContextMenuItemButton(menuIndex++,
857 TextFormat("Wall: %dG", TowerTypeGetCosts(TOWER_TYPE_WALL)),
858 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
859 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_WALL);
860
861 menu[menuCount] = ContextMenuItemButton(menuIndex++,
862 TextFormat("Archer: %dG", TowerTypeGetCosts(TOWER_TYPE_ARCHER)),
863 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
864 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_ARCHER);
865
866 menu[menuCount] = ContextMenuItemButton(menuIndex++,
867 TextFormat("Ballista: %dG", TowerTypeGetCosts(TOWER_TYPE_BALLISTA)),
868 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
869 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_BALLISTA);
870
871 menu[menuCount] = ContextMenuItemButton(menuIndex++,
872 TextFormat("Catapult: %dG", TowerTypeGetCosts(TOWER_TYPE_CATAPULT)),
873 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
874 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_CATAPULT);
875 }
876
877 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
878 {
879 level->placementContextMenuStatus = -1;
880 }
881 }
882
883 void DrawLevelBuildingState(Level *level)
884 {
885 // when the context menu is not active, we update the placement position
886 if (level->placementContextMenuStatus == 0)
887 {
888 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
889 float hitDistance = ray.position.y / -ray.direction.y;
890 float hitX = ray.direction.x * hitDistance + ray.position.x;
891 float hitY = ray.direction.z * hitDistance + ray.position.z;
892 level->placementX = (int)floorf(hitX + 0.5f);
893 level->placementY = (int)floorf(hitY + 0.5f);
894 }
895
896 // the currently hovered/selected tower
897 Tower *tower = TowerGetAt(level->placementX, level->placementY);
898 // show the range of the tower when hovering/selecting it
899 TowerUpdateAllRangeFade(tower, 0.0f);
900
901 BeginMode3D(level->camera);
902 DrawLevelGround(level);
903 PathFindingMapUpdate(0, 0);
904 TowerDrawAll();
905 EnemyDraw();
906 ProjectileDraw();
907 ParticleDraw();
908 DrawEnemyPaths(level);
909
910 guiState.isBlocked = 0;
911
912 // Hover rectangle, when the mouse is over the map
913 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
914 if (isHovering)
915 {
916 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
917 }
918
919 EndMode3D();
920
921 TowerDrawAllHealthBars(level->camera);
922
923 DrawTitle("Building phase");
924
925 // Draw the context menu when the context menu is active
926 if (level->placementContextMenuStatus >= 1)
927 {
928 float maxHitpoints = 0.0f;
929 float hp = 0.0f;
930 float damageFactor = 0.0f;
931 int32_t sellValue = 0;
932
933 if (tower)
934 {
935 maxHitpoints = TowerGetMaxHealth(tower);
936 hp = maxHitpoints - tower->damage;
937 damageFactor = 1.0f - tower->damage / maxHitpoints;
938 sellValue = (int32_t) ceilf(TowerTypeGetCosts(tower->towerType) * 0.5f * damageFactor);
939 }
940
941 ContextMenuItem menu[12] = {0};
942 int menuCount = 0;
943 int menuIndex = 0;
944 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
945 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
946
947 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
948 {
949 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
950 }
951 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
952 {
953 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
954 int costs = totalLevel * 4;
955 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
956 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
957 TowerTypeGetName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
958 int buttonMenuIndex = menuIndex;
959 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
960 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
961 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
962 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
963 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
964 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
965
966 // check if buttons should be disabled
967 if (isMaxLevel || level->playerGold < costs)
968 {
969 for (int i = buttonMenuIndex; i < menuCount; i++)
970 {
971 menu[i].buttonState.isDisabled = 1;
972 }
973 }
974
975 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
976 {
977 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
978 }
979 }
980 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
981 {
982 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", TowerTypeGetName(tower->towerType)), 0.5f);
983 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
984 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
985 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
986 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
987 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
988 {
989 level->placementContextMenuStatus = -1;
990 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
991 }
992 }
993 }
994
995 // Activate the context menu when the mouse is clicked and the context menu is not active
996 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
997 {
998 level->placementContextMenuStatus += 1;
999 }
1000
1001 if (level->placementContextMenuStatus == 0)
1002 {
1003 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1004 {
1005 level->nextState = LEVEL_STATE_RESET;
1006 }
1007
1008 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1009 {
1010 level->nextState = LEVEL_STATE_BATTLE;
1011 }
1012
1013 }
1014 }
1015
1016 void InitBattleStateConditions(Level *level)
1017 {
1018 level->state = LEVEL_STATE_BATTLE;
1019 level->nextState = LEVEL_STATE_NONE;
1020 level->waveEndTimer = 0.0f;
1021 for (int i = 0; i < 10; i++)
1022 {
1023 EnemyWave *wave = &level->waves[i];
1024 wave->spawned = 0;
1025 wave->timeToSpawnNext = wave->delay;
1026 }
1027 }
1028
1029 void DrawLevelBattleState(Level *level)
1030 {
1031 BeginMode3D(level->camera);
1032 DrawLevelGround(level);
1033 TowerUpdateAllRangeFade(0, 0.0f);
1034 TowerDrawAll();
1035 EnemyDraw();
1036 ProjectileDraw();
1037 ParticleDraw();
1038 guiState.isBlocked = 0;
1039 EndMode3D();
1040
1041 EnemyDrawHealthbars(level->camera);
1042 TowerDrawAllHealthBars(level->camera);
1043
1044 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1045 {
1046 level->nextState = LEVEL_STATE_RESET;
1047 }
1048
1049 int maxCount = 0;
1050 int remainingCount = 0;
1051 for (int i = 0; i < 10; i++)
1052 {
1053 EnemyWave *wave = &level->waves[i];
1054 if (wave->wave != level->currentWave)
1055 {
1056 continue;
1057 }
1058 maxCount += wave->count;
1059 remainingCount += wave->count - wave->spawned;
1060 }
1061 int aliveCount = EnemyCount();
1062 remainingCount += aliveCount;
1063
1064 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1065 DrawTitle(text);
1066 }
1067
1068 void DrawLevel(Level *level)
1069 {
1070 switch (level->state)
1071 {
1072 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1073 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1074 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1075 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1076 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1077 default: break;
1078 }
1079
1080 DrawLevelHud(level);
1081 }
1082
1083 void UpdateLevel(Level *level)
1084 {
1085 if (level->state == LEVEL_STATE_BATTLE)
1086 {
1087 int activeWaves = 0;
1088 for (int i = 0; i < 10; i++)
1089 {
1090 EnemyWave *wave = &level->waves[i];
1091 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1092 {
1093 continue;
1094 }
1095 activeWaves++;
1096 wave->timeToSpawnNext -= gameTime.deltaTime;
1097 if (wave->timeToSpawnNext <= 0.0f)
1098 {
1099 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1100 if (enemy)
1101 {
1102 wave->timeToSpawnNext = wave->interval;
1103 wave->spawned++;
1104 }
1105 }
1106 }
1107 if (TowerGetByType(TOWER_TYPE_BASE) == 0) {
1108 level->waveEndTimer += gameTime.deltaTime;
1109 if (level->waveEndTimer >= 2.0f)
1110 {
1111 level->nextState = LEVEL_STATE_LOST_WAVE;
1112 }
1113 }
1114 else if (activeWaves == 0 && EnemyCount() == 0)
1115 {
1116 level->waveEndTimer += gameTime.deltaTime;
1117 if (level->waveEndTimer >= 2.0f)
1118 {
1119 level->nextState = LEVEL_STATE_WON_WAVE;
1120 }
1121 }
1122 }
1123
1124 PathFindingMapUpdate(0, 0);
1125 EnemyUpdate();
1126 TowerUpdate();
1127 ProjectileUpdate();
1128 ParticleUpdate();
1129
1130 if (level->nextState == LEVEL_STATE_RESET)
1131 {
1132 InitLevel(level);
1133 }
1134
1135 if (level->nextState == LEVEL_STATE_BATTLE)
1136 {
1137 InitBattleStateConditions(level);
1138 }
1139
1140 if (level->nextState == LEVEL_STATE_WON_WAVE)
1141 {
1142 level->currentWave++;
1143 level->state = LEVEL_STATE_WON_WAVE;
1144 }
1145
1146 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1147 {
1148 level->state = LEVEL_STATE_LOST_WAVE;
1149 }
1150
1151 if (level->nextState == LEVEL_STATE_BUILDING)
1152 {
1153 level->state = LEVEL_STATE_BUILDING;
1154 level->placementContextMenuStatus = 0;
1155 }
1156
1157 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1158 {
1159 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1160 level->placementTransitionPosition = (Vector2){
1161 level->placementX, level->placementY};
1162 // initialize the spring to the current position
1163 level->placementTowerSpring = (PhysicsPoint){
1164 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1165 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1166 };
1167 level->placementPhase = PLACEMENT_PHASE_STARTING;
1168 level->placementTimer = 0.0f;
1169 }
1170
1171 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1172 {
1173 // make something of this later
1174 InitLevel(level);
1175 }
1176
1177 level->nextState = LEVEL_STATE_NONE;
1178 }
1179
1180 float nextSpawnTime = 0.0f;
1181
1182 void ResetGame()
1183 {
1184 InitLevel(currentLevel);
1185 }
1186
1187 void InitGame()
1188 {
1189 TowerInit();
1190 EnemyInit();
1191 ProjectileInit();
1192 ParticleInit();
1193 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1194
1195 currentLevel = levels;
1196 InitLevel(currentLevel);
1197 }
1198
1199 //# Immediate GUI functions
1200
1201 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1202 {
1203 const float healthBarHeight = 6.0f;
1204 const float healthBarOffset = 15.0f;
1205 const float inset = 2.0f;
1206 const float innerWidth = healthBarWidth - inset * 2;
1207 const float innerHeight = healthBarHeight - inset * 2;
1208
1209 Vector2 screenPos = GetWorldToScreen(position, camera);
1210 screenPos = Vector2Add(screenPos, screenOffset);
1211 float centerX = screenPos.x - healthBarWidth * 0.5f;
1212 float topY = screenPos.y - healthBarOffset;
1213 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1214 float healthWidth = innerWidth * healthRatio;
1215 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1216 }
1217
1218 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1219 {
1220 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1221
1222 DrawTextEx(gameFontNormal, text, (Vector2){
1223 x + (width - textSize.x) * alignX,
1224 y + (height - textSize.y) * alignY
1225 }, gameFontNormal.baseSize, 1, textColor);
1226 }
1227
1228 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1229 {
1230 Rectangle bounds = {x, y, width, height};
1231 int isPressed = 0;
1232 int isSelected = state && state->isSelected;
1233 int isDisabled = state && state->isDisabled;
1234 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1235 {
1236 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1237 {
1238 isPressed = 1;
1239 }
1240 guiState.isBlocked = 1;
1241 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1242 bounds, Vector2Zero(), 0, WHITE);
1243 }
1244 else
1245 {
1246 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1247 bounds, Vector2Zero(), 0, WHITE);
1248 }
1249 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1250 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1251 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1252 return isPressed;
1253 }
1254
1255 //# Main game loop
1256
1257 void GameUpdate()
1258 {
1259 UpdateLevel(currentLevel);
1260 }
1261
1262 int main(void)
1263 {
1264 int screenWidth, screenHeight;
1265 GetPreferredSize(&screenWidth, &screenHeight);
1266 InitWindow(screenWidth, screenHeight, "Tower defense");
1267 float gamespeed = 1.0f;
1268 int frameRate = 30;
1269 SetTargetFPS(30);
1270
1271 LoadAssets();
1272 InitGame();
1273
1274 float pause = 1.0f;
1275
1276 while (!WindowShouldClose())
1277 {
1278 if (IsPaused()) {
1279 // canvas is not visible in browser - do nothing
1280 continue;
1281 }
1282
1283 if (IsKeyPressed(KEY_F))
1284 {
1285 frameRate = (frameRate + 5) % 30;
1286 frameRate = frameRate < 10 ? 10 : frameRate;
1287 SetTargetFPS(frameRate);
1288 }
1289
1290 if (IsKeyPressed(KEY_T))
1291 {
1292 gamespeed += 0.1f;
1293 if (gamespeed > 1.05f) gamespeed = 0.1f;
1294 }
1295
1296 if (IsKeyPressed(KEY_P))
1297 {
1298 pause = pause > 0.5f ? 0.0f : 1.0f;
1299 }
1300
1301 float dt = GetFrameTime() * gamespeed * pause;
1302 // cap maximum delta time to 0.1 seconds to prevent large time steps
1303 if (dt > 0.1f) dt = 0.1f;
1304 gameTime.time += dt;
1305 gameTime.deltaTime = dt;
1306 gameTime.frameCount += 1;
1307
1308 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1309 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1310
1311 BeginDrawing();
1312 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1313
1314 GameUpdate();
1315 DrawLevel(currentLevel);
1316
1317 if (gamespeed != 1.0f)
1318 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1319 EndDrawing();
1320
1321 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1322 }
1323
1324 CloseWindow();
1325
1326 return 0;
1327 }
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 maxUpgradeDamage;
56 float areaDamageRadius;
57 float pushbackPowerDistance;
58 } HitEffectConfig;
59
60 typedef struct TowerTypeConfig
61 {
62 const char *name;
63 float cooldown;
64 float maxUpgradeCooldown;
65 float range;
66 float maxUpgradeRange;
67 float projectileSpeed;
68
69 uint8_t cost;
70 uint8_t projectileType;
71 uint16_t maxHealth;
72
73 HitEffectConfig hitEffect;
74 } TowerTypeConfig;
75
76 #define TOWER_MAX_STAGE 10
77
78 typedef struct TowerUpgradeState
79 {
80 uint8_t range;
81 uint8_t damage;
82 uint8_t speed;
83 } TowerUpgradeState;
84
85 typedef struct Tower
86 {
87 int16_t x, y;
88 uint8_t towerType;
89 TowerUpgradeState upgradeState;
90 Vector2 lastTargetPosition;
91 float cooldown;
92 float damage;
93 // alpha value for the range circle drawing
94 float drawRangeAlpha;
95 } Tower;
96
97 typedef struct GameTime
98 {
99 float time;
100 float deltaTime;
101 uint32_t frameCount;
102
103 float fixedDeltaTime;
104 // leaving the fixed time stepping to the update functions,
105 // we need to know the fixed time at the start of the frame
106 float fixedTimeStart;
107 // and the number of fixed steps that we have to make this frame
108 // The fixedTime is fixedTimeStart + n * fixedStepCount
109 uint8_t fixedStepCount;
110 } GameTime;
111
112 typedef struct ButtonState {
113 char isSelected;
114 char isDisabled;
115 } ButtonState;
116
117 typedef struct GUIState {
118 int isBlocked;
119 } GUIState;
120
121 typedef enum LevelState
122 {
123 LEVEL_STATE_NONE,
124 LEVEL_STATE_BUILDING,
125 LEVEL_STATE_BUILDING_PLACEMENT,
126 LEVEL_STATE_BATTLE,
127 LEVEL_STATE_WON_WAVE,
128 LEVEL_STATE_LOST_WAVE,
129 LEVEL_STATE_WON_LEVEL,
130 LEVEL_STATE_RESET,
131 } LevelState;
132
133 typedef struct EnemyWave {
134 uint8_t enemyType;
135 uint8_t wave;
136 uint16_t count;
137 float interval;
138 float delay;
139 Vector2 spawnPosition;
140
141 uint16_t spawned;
142 float timeToSpawnNext;
143 } EnemyWave;
144
145 #define ENEMY_MAX_WAVE_COUNT 10
146
147 typedef enum PlacementPhase
148 {
149 PLACEMENT_PHASE_STARTING,
150 PLACEMENT_PHASE_MOVING,
151 PLACEMENT_PHASE_PLACING,
152 } PlacementPhase;
153
154 typedef struct Level
155 {
156 int seed;
157 LevelState state;
158 LevelState nextState;
159 Camera3D camera;
160 int placementMode;
161 PlacementPhase placementPhase;
162 float placementTimer;
163
164 int16_t placementX;
165 int16_t placementY;
166 int8_t placementContextMenuStatus;
167 int8_t placementContextMenuType;
168
169 Vector2 placementTransitionPosition;
170 PhysicsPoint placementTowerSpring;
171
172 int initialGold;
173 int playerGold;
174
175 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
176 int currentWave;
177 float waveEndTimer;
178 } Level;
179
180 typedef struct DeltaSrc
181 {
182 char x, y;
183 } DeltaSrc;
184
185 typedef struct PathfindingMap
186 {
187 int width, height;
188 float scale;
189 float *distances;
190 long *towerIndex;
191 DeltaSrc *deltaSrc;
192 float maxDistance;
193 Matrix toMapSpace;
194 Matrix toWorldSpace;
195 } PathfindingMap;
196
197 // when we execute the pathfinding algorithm, we need to store the active nodes
198 // in a queue. Each node has a position, a distance from the start, and the
199 // position of the node that we came from.
200 typedef struct PathfindingNode
201 {
202 int16_t x, y, fromX, fromY;
203 float distance;
204 } PathfindingNode;
205
206 typedef struct EnemyId
207 {
208 uint16_t index;
209 uint16_t generation;
210 } EnemyId;
211
212 typedef struct EnemyClassConfig
213 {
214 float speed;
215 float health;
216 float shieldHealth;
217 float shieldDamageAbsorption;
218 float radius;
219 float maxAcceleration;
220 float requiredContactTime;
221 float explosionDamage;
222 float explosionRange;
223 float explosionPushbackPower;
224 int goldValue;
225 } EnemyClassConfig;
226
227 typedef struct Enemy
228 {
229 int16_t currentX, currentY;
230 int16_t nextX, nextY;
231 Vector2 simPosition;
232 Vector2 simVelocity;
233 uint16_t generation;
234 float walkedDistance;
235 float startMovingTime;
236 float damage, futureDamage;
237 float shieldDamage;
238 float contactTime;
239 uint8_t enemyType;
240 uint8_t movePathCount;
241 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
242 } Enemy;
243
244 // a unit that uses sprites to be drawn
245 #define SPRITE_UNIT_ANIMATION_COUNT 6
246 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
247 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
248 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
249
250 typedef struct SpriteAnimation
251 {
252 Rectangle srcRect;
253 Vector2 offset;
254 uint8_t animationId;
255 uint8_t frameCount;
256 uint8_t frameWidth;
257 float frameDuration;
258 } SpriteAnimation;
259
260 typedef struct SpriteUnit
261 {
262 float scale;
263 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
264 } SpriteUnit;
265
266 #define PROJECTILE_MAX_COUNT 1200
267 #define PROJECTILE_TYPE_NONE 0
268 #define PROJECTILE_TYPE_ARROW 1
269 #define PROJECTILE_TYPE_CATAPULT 2
270 #define PROJECTILE_TYPE_BALLISTA 3
271
272 typedef struct Projectile
273 {
274 uint8_t projectileType;
275 float shootTime;
276 float arrivalTime;
277 float distance;
278 Vector3 position;
279 Vector3 target;
280 Vector3 directionNormal;
281 EnemyId targetEnemy;
282 HitEffectConfig hitEffectConfig;
283 } Projectile;
284
285 //# Function declarations
286 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
287 int EnemyAddDamageRange(Vector2 position, float range, float damage);
288 int EnemyAddDamage(Enemy *enemy, float damage);
289
290 //# Enemy functions
291 void EnemyInit();
292 void EnemyDraw();
293 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
294 void EnemyUpdate();
295 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
296 float EnemyGetMaxHealth(Enemy *enemy);
297 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
298 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
299 EnemyId EnemyGetId(Enemy *enemy);
300 Enemy *EnemyTryResolve(EnemyId enemyId);
301 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
302 int EnemyAddDamage(Enemy *enemy, float damage);
303 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
304 int EnemyCount();
305 void EnemyDrawHealthbars(Camera3D camera);
306
307 //# Tower functions
308 const char *TowerTypeGetName(uint8_t towerType);
309 int TowerTypeGetCosts(uint8_t towerType);
310 void TowerInit();
311 void TowerUpdate();
312 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget);
313 void TowerDrawAll();
314 void TowerDrawAllHealthBars(Camera3D camera);
315 void TowerDrawModel(Tower *tower);
316 void TowerDrawRange(Tower *tower, float alpha);
317 Tower *TowerGetByIndex(int index);
318 Tower *TowerGetByType(uint8_t towerType);
319 Tower *TowerGetAt(int16_t x, int16_t y);
320 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
321 float TowerGetMaxHealth(Tower *tower);
322 float TowerGetRange(Tower *tower);
323
324 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
325
326 //# Particles
327 void ParticleInit();
328 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
329 void ParticleUpdate();
330 void ParticleDraw();
331
332 //# Projectiles
333 void ProjectileInit();
334 void ProjectileDraw();
335 void ProjectileUpdate();
336 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
337
338 //# Pathfinding map
339 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
340 float PathFindingGetDistance(int mapX, int mapY);
341 Vector2 PathFindingGetGradient(Vector3 world);
342 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
343 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
344 void PathFindingMapDraw();
345
346 //# UI
347 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
348
349 //# Level
350 void DrawLevelGround(Level *level);
351 void DrawEnemyPath(Level *level, Color arrowColor);
352
353 //# variables
354 extern Level *currentLevel;
355 extern Enemy enemies[ENEMY_MAX_COUNT];
356 extern int enemyCount;
357 extern EnemyClassConfig enemyClassConfigs[];
358
359 extern GUIState guiState;
360 extern GameTime gameTime;
361 extern Tower towers[TOWER_MAX_COUNT];
362 extern int towerCount;
363
364 extern Texture2D palette, spriteSheet;
365
366 #endif
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 .maxUpgradeCooldown = 0.25f,
13 .range = 3.0f,
14 .maxUpgradeRange = 5.0f,
15 .cost = 6,
16 .maxHealth = 10,
17 .projectileSpeed = 4.0f,
18 .projectileType = PROJECTILE_TYPE_ARROW,
19 .hitEffect = {
20 .damage = 3.0f,
21 .maxUpgradeDamage = 6.0f,
22 },
23 },
24 [TOWER_TYPE_BALLISTA] = {
25 .name = "Ballista",
26 .cooldown = 1.5f,
27 .maxUpgradeCooldown = 1.0f,
28 .range = 6.0f,
29 .maxUpgradeRange = 8.0f,
30 .cost = 9,
31 .maxHealth = 10,
32 .projectileSpeed = 10.0f,
33 .projectileType = PROJECTILE_TYPE_BALLISTA,
34 .hitEffect = {
35 .damage = 8.0f,
36 .maxUpgradeDamage = 16.0f,
37 .pushbackPowerDistance = 0.25f,
38 }
39 },
40 [TOWER_TYPE_CATAPULT] = {
41 .name = "Catapult",
42 .cooldown = 1.7f,
43 .maxUpgradeCooldown = 1.0f,
44 .range = 5.0f,
45 .maxUpgradeRange = 7.0f,
46 .cost = 10,
47 .maxHealth = 10,
48 .projectileSpeed = 3.0f,
49 .projectileType = PROJECTILE_TYPE_CATAPULT,
50 .hitEffect = {
51 .damage = 2.0f,
52 .maxUpgradeDamage = 4.0f,
53 .areaDamageRadius = 1.75f,
54 }
55 },
56 [TOWER_TYPE_WALL] = {
57 .name = "Wall",
58 .cost = 2,
59 .maxHealth = 10,
60 },
61 };
62
63 Tower towers[TOWER_MAX_COUNT];
64 int towerCount = 0;
65
66 Model towerModels[TOWER_TYPE_COUNT];
67
68 // definition of our archer unit
69 SpriteUnit archerUnit = {
70 .animations[0] = {
71 .srcRect = {0, 0, 16, 16},
72 .offset = {7, 1},
73 .frameCount = 1,
74 .frameDuration = 0.0f,
75 },
76 .animations[1] = {
77 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
78 .srcRect = {16, 0, 6, 16},
79 .offset = {8, 0},
80 },
81 .animations[2] = {
82 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
83 .srcRect = {22, 0, 11, 16},
84 .offset = {10, 0},
85 },
86 };
87
88 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
89 {
90 float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
91 float xScale = flip ? -1.0f : 1.0f;
92 Camera3D camera = currentLevel->camera;
93 float size = 0.5f * unitScale;
94 // we want the sprite to face the camera, so we need to calculate the up vector
95 Vector3 forward = Vector3Subtract(camera.target, camera.position);
96 Vector3 up = {0, 1, 0};
97 Vector3 right = Vector3CrossProduct(forward, up);
98 up = Vector3Normalize(Vector3CrossProduct(right, forward));
99
100 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
101 {
102 SpriteAnimation anim = unit.animations[i];
103 if (anim.animationId != phase && anim.animationId != 0)
104 {
105 continue;
106 }
107 Rectangle srcRect = anim.srcRect;
108 if (anim.frameCount > 1)
109 {
110 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
111 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
112 }
113 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
114 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
115
116 if (flip)
117 {
118 srcRect.x += srcRect.width;
119 srcRect.width = -srcRect.width;
120 offset.x = scale.x - offset.x;
121 }
122 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
123 // move the sprite slightly towards the camera to avoid z-fighting
124 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
125 }
126 }
127
128 void TowerInit()
129 {
130 for (int i = 0; i < TOWER_MAX_COUNT; i++)
131 {
132 towers[i] = (Tower){0};
133 }
134 towerCount = 0;
135
136 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
137 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
138
139 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
140 {
141 if (towerModels[i].materials)
142 {
143 // assign the palette texture to the material of the model (0 is not used afaik)
144 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
145 }
146 }
147 }
148
149 static float TowerGetCooldown(Tower *tower)
150 {
151 float cooldown = towerTypeConfigs[tower->towerType].cooldown;
152 float maxUpgradeCooldown = towerTypeConfigs[tower->towerType].maxUpgradeCooldown;
153 if (tower->upgradeState.speed > 0)
154 {
155 cooldown = Lerp(cooldown, maxUpgradeCooldown, tower->upgradeState.speed / (float)TOWER_MAX_STAGE);
156 }
157 return cooldown;
158 }
159
160 static void TowerGunUpdate(Tower *tower)
161 {
162 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
163 if (tower->cooldown <= 0.0f)
164 {
165 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, TowerGetRange(tower));
166 if (enemy)
167 {
168 tower->cooldown = TowerGetCooldown(tower);
169 // shoot the enemy; determine future position of the enemy
170 float bulletSpeed = config.projectileSpeed;
171 Vector2 velocity = enemy->simVelocity;
172 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
173 Vector2 towerPosition = {tower->x, tower->y};
174 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
175 for (int i = 0; i < 8; i++) {
176 velocity = enemy->simVelocity;
177 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
178 float distance = Vector2Distance(towerPosition, futurePosition);
179 float eta2 = distance / bulletSpeed;
180 if (fabs(eta - eta2) < 0.01f) {
181 break;
182 }
183 eta = (eta2 + eta) * 0.5f;
184 }
185
186 HitEffectConfig hitEffect = config.hitEffect;
187 // apply damage upgrade to hit effect
188 if (tower->upgradeState.damage > 0)
189 {
190 hitEffect.damage = Lerp(hitEffect.damage, hitEffect.maxUpgradeDamage, tower->upgradeState.damage / (float)TOWER_MAX_STAGE);
191 }
192
193 ProjectileTryAdd(config.projectileType, enemy,
194 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
195 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
196 bulletSpeed, hitEffect);
197 enemy->futureDamage += hitEffect.damage;
198 tower->lastTargetPosition = futurePosition;
199 }
200 }
201 else
202 {
203 tower->cooldown -= gameTime.deltaTime;
204 }
205 }
206
207 Tower *TowerGetAt(int16_t x, int16_t y)
208 {
209 for (int i = 0; i < towerCount; i++)
210 {
211 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
212 {
213 return &towers[i];
214 }
215 }
216 return 0;
217 }
218
219 Tower *TowerGetByIndex(int index)
220 {
221 if (index < 0 || index >= towerCount)
222 {
223 return 0;
224 }
225 return &towers[index];
226 }
227
228 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
229 {
230 if (towerCount >= TOWER_MAX_COUNT)
231 {
232 return 0;
233 }
234
235 Tower *tower = TowerGetAt(x, y);
236 if (tower)
237 {
238 return 0;
239 }
240
241 tower = &towers[towerCount++];
242 *tower = (Tower){
243 .x = x,
244 .y = y,
245 .towerType = towerType,
246 .cooldown = 0.0f,
247 .damage = 0.0f,
248 };
249 return tower;
250 }
251
252 Tower *TowerGetByType(uint8_t towerType)
253 {
254 for (int i = 0; i < towerCount; i++)
255 {
256 if (towers[i].towerType == towerType)
257 {
258 return &towers[i];
259 }
260 }
261 return 0;
262 }
263
264 const char *TowerTypeGetName(uint8_t towerType)
265 {
266 return towerTypeConfigs[towerType].name;
267 }
268
269 int TowerTypeGetCosts(uint8_t towerType)
270 {
271 return towerTypeConfigs[towerType].cost;
272 }
273
274 float TowerGetMaxHealth(Tower *tower)
275 {
276 return towerTypeConfigs[tower->towerType].maxHealth;
277 }
278
279 float TowerGetRange(Tower *tower)
280 {
281 float range = towerTypeConfigs[tower->towerType].range;
282 float maxUpgradeRange = towerTypeConfigs[tower->towerType].maxUpgradeRange;
283 if (tower->upgradeState.range > 0)
284 {
285 range = Lerp(range, maxUpgradeRange, tower->upgradeState.range / (float)TOWER_MAX_STAGE);
286 }
287 return range;
288 }
289
290 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget)
291 {
292 // animate fade in and fade out of range drawing using framerate independent lerp
293 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime);
294 for (int i = 0; i < TOWER_MAX_COUNT; i++)
295 {
296 Tower *fadingTower = TowerGetByIndex(i);
297 if (!fadingTower)
298 {
299 break;
300 }
301 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget;
302 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp);
303 }
304 }
305
306 void TowerDrawRange(Tower *tower, float alpha)
307 {
308 Color ringColor = (Color){255, 200, 100, 255};
309 const int rings = 4;
310 const float radiusOffset = 0.5f;
311 const float animationSpeed = 2.0f;
312 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
313 float radius = TowerGetRange(tower);
314 // base circle
315 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, radius, (Vector3){1, 0, 0}, 90,
316 Fade(ringColor, alpha));
317
318 for (int i = 1; i < rings; i++)
319 {
320 float t = ((float)i + animation) / (float)rings;
321 float r = Lerp(radius, radius - radiusOffset, t * t);
322 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
323 if (i == 1)
324 {
325 // fade out the outermost ring
326 a = animation;
327 }
328 a *= alpha;
329
330 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, r, (Vector3){1, 0, 0}, 90,
331 Fade(ringColor, a));
332 }
333 }
334
335 void TowerDrawModel(Tower *tower)
336 {
337 if (tower->towerType == TOWER_TYPE_NONE)
338 {
339 return;
340 }
341
342 if (tower->drawRangeAlpha > 2.0f/256.0f)
343 {
344 TowerDrawRange(tower, tower->drawRangeAlpha);
345 }
346
347 switch (tower->towerType)
348 {
349 case TOWER_TYPE_ARCHER:
350 {
351 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower->x, 0.0f, tower->y}, currentLevel->camera);
352 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower->lastTargetPosition.x, 0.0f, tower->lastTargetPosition.y}, currentLevel->camera);
353 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
354 DrawSpriteUnit(archerUnit, (Vector3){tower->x, 1.0f, tower->y}, 0, screenPosTarget.x > screenPosTower.x,
355 tower->cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
356 }
357 break;
358 case TOWER_TYPE_BALLISTA:
359 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, BROWN);
360 break;
361 case TOWER_TYPE_CATAPULT:
362 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
363 break;
364 default:
365 if (towerModels[tower->towerType].materials)
366 {
367 DrawModel(towerModels[tower->towerType], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
368 } else {
369 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
370 }
371 break;
372 }
373 }
374
375 void TowerDrawAll()
376 {
377 for (int i = 0; i < towerCount; i++)
378 {
379 TowerDrawModel(&towers[i]);
380 }
381 }
382
383 void TowerUpdate()
384 {
385 for (int i = 0; i < towerCount; i++)
386 {
387 Tower *tower = &towers[i];
388 switch (tower->towerType)
389 {
390 case TOWER_TYPE_CATAPULT:
391 case TOWER_TYPE_BALLISTA:
392 case TOWER_TYPE_ARCHER:
393 TowerGunUpdate(tower);
394 break;
395 }
396 }
397 }
398
399 void TowerDrawAllHealthBars(Camera3D camera)
400 {
401 for (int i = 0; i < towerCount; i++)
402 {
403 Tower *tower = &towers[i];
404 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
405 {
406 continue;
407 }
408
409 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
410 float maxHealth = TowerGetMaxHealth(tower);
411 float health = maxHealth - tower->damage;
412 float healthRatio = health / maxHealth;
413
414 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
415 }
416 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <rlgl.h>
6
7 EnemyClassConfig enemyClassConfigs[] = {
8 [ENEMY_TYPE_MINION] = {
9 .health = 10.0f,
10 .speed = 0.6f,
11 .radius = 0.25f,
12 .maxAcceleration = 1.0f,
13 .explosionDamage = 1.0f,
14 .requiredContactTime = 0.5f,
15 .explosionRange = 1.0f,
16 .explosionPushbackPower = 0.25f,
17 .goldValue = 1,
18 },
19 [ENEMY_TYPE_RUNNER] = {
20 .health = 5.0f,
21 .speed = 1.0f,
22 .radius = 0.25f,
23 .maxAcceleration = 2.0f,
24 .explosionDamage = 1.0f,
25 .requiredContactTime = 0.5f,
26 .explosionRange = 1.0f,
27 .explosionPushbackPower = 0.25f,
28 .goldValue = 2,
29 },
30 [ENEMY_TYPE_SHIELD] = {
31 .health = 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 upgrade system is now working as intended. One can argue if the upgrade mechanic makes sense, but it certainly was a simple system to implement. One way or another, the changes done here are potentially useful for other approaches as well, e.g. if we chose a different upgrade system like a skill tree, improving the range or damage of a tower would require similar changes. Now that we have the upgrade system in place, we can look into how we could balance the game.
Conclusion
In this part, we implemented the tower upgrade system. While simple, it offers potentially some room for playing strategically.
While we display now also the range of the tower, we currently do not display the damage or rate of fire. This is something we could add in the future.
In the next part, the focus will look into parsing the game configuration from a file.