Simple tower defense tutorial, part 21: Config loader
In the previous part we implemented a simple parser for the game configuration.
The plan for this part is to integrate the parser into the game and load the configuration from a file.
The last sample in this post should allow modifying the configuration file and reloading the game without recompiling it in the web browser.
Loading the configuration
The first step is to load the configuration file when the game starts. The parser needs a few modifications to cooperate with the game, but in general, it is almost the same as in the previous post.
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 loadedLevels[32] = {0};
60 Level levels[] = {
61 [0] = {
62 .state = LEVEL_STATE_BUILDING,
63 .initialGold = 500,
64 .waves[0] = {
65 .enemyType = ENEMY_TYPE_SHIELD,
66 .wave = 0,
67 .count = 1,
68 .interval = 2.5f,
69 .delay = 1.0f,
70 .spawnPosition = {2, 6},
71 },
72 .waves[1] = {
73 .enemyType = ENEMY_TYPE_RUNNER,
74 .wave = 0,
75 .count = 5,
76 .interval = 0.5f,
77 .delay = 1.0f,
78 .spawnPosition = {-2, 6},
79 },
80 .waves[2] = {
81 .enemyType = ENEMY_TYPE_SHIELD,
82 .wave = 1,
83 .count = 20,
84 .interval = 1.5f,
85 .delay = 1.0f,
86 .spawnPosition = {0, 6},
87 },
88 .waves[3] = {
89 .enemyType = ENEMY_TYPE_MINION,
90 .wave = 2,
91 .count = 30,
92 .interval = 1.2f,
93 .delay = 1.0f,
94 .spawnPosition = {2, 6},
95 },
96 .waves[4] = {
97 .enemyType = ENEMY_TYPE_BOSS,
98 .wave = 2,
99 .count = 2,
100 .interval = 5.0f,
101 .delay = 2.0f,
102 .spawnPosition = {-2, 4},
103 }
104 },
105 };
106
107 Level *currentLevel = levels;
108
109 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
110 void LoadConfig();
111
112 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
113 {
114 int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
115 int panelWidth = textWidth + 40;
116 int posX = anchorX - panelWidth * alignX;
117 int textOffset = 20;
118 DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
119 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
120 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
121 }
122
123 void DrawTitle(const char *text)
124 {
125 DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
126 }
127
128 //# Game
129
130 static Model LoadGLBModel(char *filename)
131 {
132 Model model = LoadModel(TextFormat("data/%s.glb",filename));
133 for (int i = 0; i < model.materialCount; i++)
134 {
135 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
136 }
137 return model;
138 }
139
140 void LoadAssets()
141 {
142 // load a sprite sheet that contains all units
143 spriteSheet = LoadTexture("data/spritesheet.png");
144 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
145
146 // we'll use a palette texture to colorize the all buildings and environment art
147 palette = LoadTexture("data/palette.png");
148 // The texture uses gradients on very small space, so we'll enable bilinear filtering
149 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
150
151 gameFontNormal = LoadFont("data/alagard.png");
152
153 floorTileAModel = LoadGLBModel("floor-tile-a");
154 floorTileBModel = LoadGLBModel("floor-tile-b");
155 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
156 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
157 firTreeModel[0] = LoadGLBModel("firtree-1-a");
158 firTreeModel[1] = LoadGLBModel("firtree-1-b");
159 rockModels[0] = LoadGLBModel("rock-1");
160 rockModels[1] = LoadGLBModel("rock-2");
161 rockModels[2] = LoadGLBModel("rock-3");
162 rockModels[3] = LoadGLBModel("rock-4");
163 rockModels[4] = LoadGLBModel("rock-5");
164 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
165
166 pathArrowModel = LoadGLBModel("direction-arrow-x");
167 greenArrowModel = LoadGLBModel("green-arrow");
168 }
169
170 void InitLevel(Level *level)
171 {
172 level->seed = (int)(GetTime() * 100.0f);
173
174 TowerInit();
175 EnemyInit();
176 ProjectileInit();
177 ParticleInit();
178 LoadConfig();
179 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
180
181 level->placementMode = 0;
182 level->state = LEVEL_STATE_BUILDING;
183 level->nextState = LEVEL_STATE_NONE;
184 level->playerGold = level->initialGold;
185 level->currentWave = 0;
186 level->placementX = -1;
187 level->placementY = 0;
188
189 Camera *camera = &level->camera;
190 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
191 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
192 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
193 camera->fovy = 11.5f;
194 camera->projection = CAMERA_ORTHOGRAPHIC;
195 }
196
197 void DrawLevelHud(Level *level)
198 {
199 const char *text = TextFormat("Gold: %d", level->playerGold);
200 DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
201 }
202
203 void DrawLevelReportLostWave(Level *level)
204 {
205 BeginMode3D(level->camera);
206 DrawLevelGround(level);
207 TowerUpdateAllRangeFade(0, 0.0f);
208 TowerDrawAll();
209 EnemyDraw();
210 ProjectileDraw();
211 ParticleDraw();
212 guiState.isBlocked = 0;
213 EndMode3D();
214
215 TowerDrawAllHealthBars(level->camera);
216
217 DrawTitle("Wave lost");
218
219 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
220 {
221 level->nextState = LEVEL_STATE_RESET;
222 }
223 }
224
225 int HasLevelNextWave(Level *level)
226 {
227 for (int i = 0; i < 10; i++)
228 {
229 EnemyWave *wave = &level->waves[i];
230 if (wave->wave == level->currentWave)
231 {
232 return 1;
233 }
234 }
235 return 0;
236 }
237
238 void DrawLevelReportWonWave(Level *level)
239 {
240 BeginMode3D(level->camera);
241 DrawLevelGround(level);
242 TowerUpdateAllRangeFade(0, 0.0f);
243 TowerDrawAll();
244 EnemyDraw();
245 ProjectileDraw();
246 ParticleDraw();
247 guiState.isBlocked = 0;
248 EndMode3D();
249
250 TowerDrawAllHealthBars(level->camera);
251
252 DrawTitle("Wave won");
253
254
255 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
256 {
257 level->nextState = LEVEL_STATE_RESET;
258 }
259
260 if (HasLevelNextWave(level))
261 {
262 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
263 {
264 level->nextState = LEVEL_STATE_BUILDING;
265 }
266 }
267 else {
268 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
269 {
270 level->nextState = LEVEL_STATE_WON_LEVEL;
271 }
272 }
273 }
274
275 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
276 {
277 static ButtonState buttonStates[8] = {0};
278 int cost = TowerTypeGetCosts(towerType);
279 const char *text = TextFormat("%s: %d", name, cost);
280 buttonStates[towerType].isSelected = level->placementMode == towerType;
281 buttonStates[towerType].isDisabled = level->playerGold < cost;
282 if (Button(text, x, y, width, height, &buttonStates[towerType]))
283 {
284 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
285 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
286 return 1;
287 }
288 return 0;
289 }
290
291 float GetRandomFloat(float min, float max)
292 {
293 int random = GetRandomValue(0, 0xfffffff);
294 return ((float)random / (float)0xfffffff) * (max - min) + min;
295 }
296
297 void DrawLevelGround(Level *level)
298 {
299 // draw checkerboard ground pattern
300 for (int x = -5; x <= 5; x += 1)
301 {
302 for (int y = -5; y <= 5; y += 1)
303 {
304 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
305 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
306 }
307 }
308
309 int oldSeed = GetRandomValue(0, 0xfffffff);
310 SetRandomSeed(level->seed);
311 // increase probability for trees via duplicated entries
312 Model borderModels[64];
313 int maxRockCount = GetRandomValue(2, 6);
314 int maxTreeCount = GetRandomValue(10, 20);
315 int maxFirTreeCount = GetRandomValue(5, 10);
316 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
317 int grassPatchCount = GetRandomValue(5, 30);
318
319 int modelCount = 0;
320 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
321 {
322 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
323 }
324 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
325 {
326 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
327 }
328 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
329 {
330 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
331 }
332 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
333 {
334 borderModels[modelCount++] = grassPatchModel[0];
335 }
336
337 // draw some objects around the border of the map
338 Vector3 up = {0, 1, 0};
339 // a pseudo random number generator to get the same result every time
340 const float wiggle = 0.75f;
341 const int layerCount = 3;
342 for (int layer = 0; layer <= layerCount; layer++)
343 {
344 int layerPos = 6 + layer;
345 Model *selectedModels = borderModels;
346 int selectedModelCount = modelCount;
347 if (layer == 0)
348 {
349 selectedModels = grassPatchModel;
350 selectedModelCount = 1;
351 }
352 for (int x = -6 - layer; x <= 6 + layer; x += 1)
353 {
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 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
358 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
359 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
360 }
361
362 for (int z = -5 - layer; z <= 5 + layer; z += 1)
363 {
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 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
368 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
369 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
370 }
371 }
372
373 SetRandomSeed(oldSeed);
374 }
375
376 void DrawEnemyPath(Level *level, Color arrowColor)
377 {
378 const int castleX = 0, castleY = 0;
379 const int maxWaypointCount = 200;
380 const float timeStep = 1.0f;
381 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
382
383 // we start with a time offset to simulate the path,
384 // this way the arrows are animated in a forward moving direction
385 // The time is wrapped around the time step to get a smooth animation
386 float timeOffset = fmodf(GetTime(), timeStep);
387
388 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
389 {
390 EnemyWave *wave = &level->waves[i];
391 if (wave->wave != level->currentWave)
392 {
393 continue;
394 }
395
396 // use this dummy enemy to simulate the path
397 Enemy dummy = {
398 .enemyType = ENEMY_TYPE_MINION,
399 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
400 .nextX = wave->spawnPosition.x,
401 .nextY = wave->spawnPosition.y,
402 .currentX = wave->spawnPosition.x,
403 .currentY = wave->spawnPosition.y,
404 };
405
406 float deltaTime = timeOffset;
407 for (int j = 0; j < maxWaypointCount; j++)
408 {
409 int waypointPassedCount = 0;
410 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
411 // after the initial variable starting offset, we use a fixed time step
412 deltaTime = timeStep;
413 dummy.simPosition = pos;
414
415 // Update the dummy's position just like we do in the regular enemy update loop
416 for (int k = 0; k < waypointPassedCount; k++)
417 {
418 dummy.currentX = dummy.nextX;
419 dummy.currentY = dummy.nextY;
420 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
421 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
422 {
423 break;
424 }
425 }
426 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
427 {
428 break;
429 }
430
431 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
432 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
433 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
434 }
435 }
436 }
437
438 void DrawEnemyPaths(Level *level)
439 {
440 // disable depth testing for the path arrows
441 // flush the 3D batch to draw the arrows on top of everything
442 rlDrawRenderBatchActive();
443 rlDisableDepthTest();
444 DrawEnemyPath(level, (Color){64, 64, 64, 160});
445
446 rlDrawRenderBatchActive();
447 rlEnableDepthTest();
448 DrawEnemyPath(level, WHITE);
449 }
450
451 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
452 {
453 float dt = gameTime.fixedDeltaTime;
454 // smooth transition for the placement position using exponential decay
455 const float lambda = 15.0f;
456 float factor = 1.0f - expf(-lambda * dt);
457
458 float damping = 0.5f;
459 float springStiffness = 300.0f;
460 float springDecay = 95.0f;
461 float minHeight = 0.35f;
462
463 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
464 {
465 damping = 1.0f;
466 springDecay = 90.0f;
467 springStiffness = 100.0f;
468 minHeight = 0.70f;
469 }
470
471 for (int i = 0; i < gameTime.fixedStepCount; i++)
472 {
473 level->placementTransitionPosition =
474 Vector2Lerp(
475 level->placementTransitionPosition,
476 (Vector2){mapX, mapY}, factor);
477
478 // draw the spring position for debugging the spring simulation
479 // first step: stiff spring, no simulation
480 Vector3 worldPlacementPosition = (Vector3){
481 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
482 Vector3 springTargetPosition = (Vector3){
483 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
484 // consider the current velocity to predict the future position in order to dampen
485 // the spring simulation. Longer prediction times will result in more damping
486 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
487 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
488 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
489 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
490 // decay velocity of the upright forcing spring
491 // This force acts like a 2nd spring that pulls the tip upright into the air above the
492 // base position
493 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
494 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
495
496 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
497 // we use a simple spring model with a rest length of 1.0f
498 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
499 float springLength = Vector3Length(springDelta);
500 float springForce = (springLength - 1.0f) * springStiffness;
501 Vector3 springForceVector = Vector3Normalize(springDelta);
502 springForceVector = Vector3Scale(springForceVector, springForce);
503 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
504 Vector3Scale(springForceVector, dt));
505
506 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
507 Vector3Scale(level->placementTowerSpring.velocity, dt));
508 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
509 {
510 level->placementTowerSpring.velocity.y *= -1.0f;
511 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
512 }
513 }
514 }
515
516 void DrawLevelBuildingPlacementState(Level *level)
517 {
518 const float placementDuration = 0.5f;
519
520 level->placementTimer += gameTime.deltaTime;
521 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
522 {
523 level->placementPhase = PLACEMENT_PHASE_MOVING;
524 level->placementTimer = 0.0f;
525 }
526
527 BeginMode3D(level->camera);
528 DrawLevelGround(level);
529
530 int blockedCellCount = 0;
531 Vector2 blockedCells[1];
532 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
533 float planeDistance = ray.position.y / -ray.direction.y;
534 float planeX = ray.direction.x * planeDistance + ray.position.x;
535 float planeY = ray.direction.z * planeDistance + ray.position.z;
536 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
537 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
538 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
539 level->placementMode && !guiState.isBlocked &&
540 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
541 {
542 level->placementX = mapX;
543 level->placementY = mapY;
544 }
545 else
546 {
547 mapX = level->placementX;
548 mapY = level->placementY;
549 }
550 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
551 PathFindingMapUpdate(blockedCellCount, blockedCells);
552
553 TowerUpdateAllRangeFade(0, 0.0f);
554 TowerDrawAll();
555 EnemyDraw();
556 ProjectileDraw();
557 ParticleDraw();
558 DrawEnemyPaths(level);
559
560 // let the tower float up and down. Consider this height in the spring simulation as well
561 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
562
563 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
564 {
565 // The bouncing spring needs a bit of outro time to look nice and complete.
566 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
567 // and the last 1/3rd is the outro physics (bouncing)
568 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
569 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
570 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
571 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
572 towerFloatHeight = linearBlendHeight + parabola;
573 }
574
575 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
576
577 rlPushMatrix();
578 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
579
580 // calculate x and z rotation to align the model with the spring
581 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
582 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
583 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
584 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
585 float springLength = Vector3Length(towerUp);
586 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
587 float towerSquash = 1.0f / towerStretch;
588
589 Tower dummy = {
590 .towerType = level->placementMode,
591 };
592
593 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
594 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
595 {
596 rangeAlpha = 1.0f - rangeAlpha;
597 }
598 else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
599 {
600 rangeAlpha = 1.0f;
601 }
602
603 TowerDrawRange(&dummy, rangeAlpha);
604
605 rlPushMatrix();
606 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
607
608 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
609 rlScalef(towerSquash, towerStretch, towerSquash);
610 TowerDrawModel(&dummy);
611 rlPopMatrix();
612
613
614 // draw a shadow for the tower
615 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
616 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
617 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
618
619
620 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
621 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
622 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
623 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
624
625 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
626 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
627 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
628 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
629 rlPopMatrix();
630
631 guiState.isBlocked = 0;
632
633 EndMode3D();
634
635 TowerDrawAllHealthBars(level->camera);
636
637 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
638 {
639 if (level->placementTimer > placementDuration)
640 {
641 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
642 // testing repairing
643 tower->damage = 2.5f;
644 level->playerGold -= TowerTypeGetCosts(level->placementMode);
645 level->nextState = LEVEL_STATE_BUILDING;
646 level->placementMode = TOWER_TYPE_NONE;
647 }
648 }
649 else
650 {
651 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
652 {
653 level->nextState = LEVEL_STATE_BUILDING;
654 level->placementMode = TOWER_TYPE_NONE;
655 TraceLog(LOG_INFO, "Cancel building");
656 }
657
658 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
659 {
660 level->placementPhase = PLACEMENT_PHASE_PLACING;
661 level->placementTimer = 0.0f;
662 }
663 }
664 }
665
666 enum ContextMenuType
667 {
668 CONTEXT_MENU_TYPE_MAIN,
669 CONTEXT_MENU_TYPE_SELL_CONFIRM,
670 CONTEXT_MENU_TYPE_UPGRADE,
671 };
672
673 enum UpgradeType
674 {
675 UPGRADE_TYPE_SPEED,
676 UPGRADE_TYPE_DAMAGE,
677 UPGRADE_TYPE_RANGE,
678 };
679
680 typedef struct ContextMenuArgs
681 {
682 void *data;
683 uint8_t uint8;
684 int32_t int32;
685 Tower *tower;
686 } ContextMenuArgs;
687
688 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
689 {
690 uint8_t towerType = data->uint8;
691 level->placementMode = towerType;
692 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
693 return 1;
694 }
695
696 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
697 {
698 Tower *tower = data->tower;
699 int gold = data->int32;
700 level->playerGold += gold;
701 tower->towerType = TOWER_TYPE_NONE;
702 return 1;
703 }
704
705 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
706 {
707 return 1;
708 }
709
710 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
711 {
712 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
713 return 0;
714 }
715
716 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
717 {
718 Tower *tower = data->tower;
719 switch (data->uint8)
720 {
721 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
722 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
723 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
724 }
725 level->playerGold -= data->int32;
726 return 0;
727 }
728
729 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
730 {
731 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
732 return 0;
733 }
734
735 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
736 {
737 Tower *tower = data->tower;
738 if (level->playerGold >= 1)
739 {
740 level->playerGold -= 1;
741 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
742 }
743 return tower->damage == 0.0f;
744 }
745
746 typedef struct ContextMenuItem
747 {
748 uint8_t index;
749 char text[24];
750 float alignX;
751 int (*action)(Level*, ContextMenuArgs*);
752 void *data;
753 ContextMenuArgs args;
754 ButtonState buttonState;
755 } ContextMenuItem;
756
757 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
758 {
759 ContextMenuItem item = {.index = index, .alignX = alignX};
760 strncpy(item.text, text, 23);
761 item.text[23] = 0;
762 return item;
763 }
764
765 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
766 {
767 ContextMenuItem item = {.index = index, .action = action, .args = args};
768 strncpy(item.text, text, 23);
769 item.text[23] = 0;
770 return item;
771 }
772
773 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
774 {
775 const int itemHeight = 28;
776 const int itemSpacing = 1;
777 const int padding = 8;
778 int itemCount = 0;
779 for (int i = 0; menus[i].text[0]; i++)
780 {
781 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
782 }
783
784 Rectangle contextMenu = {0, 0, width,
785 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
786
787 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
788 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
789
790 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
791 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
792 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
793 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
794
795 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
796 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
797 const int itemX = contextMenu.x + itemSpacing;
798 const int itemWidth = contextMenu.width - itemSpacing * 2;
799 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
800 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
801 int status = 0;
802 for (int i = 0; menus[i].text[0]; i++)
803 {
804 if (menus[i].action)
805 {
806 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
807 {
808 status = menus[i].action(level, &menus[i].args);
809 }
810 }
811 else
812 {
813 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
814 }
815 }
816
817 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
818 {
819 return 1;
820 }
821
822 return status;
823 }
824
825 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
826 {
827 ContextMenuItem menu[12] = {0};
828 int menuCount = 0;
829 int menuIndex = 0;
830 if (tower)
831 {
832
833 if (tower) {
834 menu[menuCount++] = ContextMenuItemText(menuIndex++, TowerTypeGetName(tower->towerType), 0.5f);
835 }
836
837 // two texts, same line
838 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
839 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
840
841 if (tower->towerType != TOWER_TYPE_BASE)
842 {
843 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
844 (ContextMenuArgs){.tower = tower});
845 }
846
847 if (tower->towerType != TOWER_TYPE_BASE)
848 {
849
850 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
851 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
852 }
853 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
854 {
855 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
856 (ContextMenuArgs){.tower = tower});
857 }
858 }
859 else
860 {
861 menu[menuCount] = ContextMenuItemButton(menuIndex++,
862 TextFormat("Wall: %dG", TowerTypeGetCosts(TOWER_TYPE_WALL)),
863 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
864 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_WALL);
865
866 menu[menuCount] = ContextMenuItemButton(menuIndex++,
867 TextFormat("Archer: %dG", TowerTypeGetCosts(TOWER_TYPE_ARCHER)),
868 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
869 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_ARCHER);
870
871 menu[menuCount] = ContextMenuItemButton(menuIndex++,
872 TextFormat("Ballista: %dG", TowerTypeGetCosts(TOWER_TYPE_BALLISTA)),
873 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
874 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_BALLISTA);
875
876 menu[menuCount] = ContextMenuItemButton(menuIndex++,
877 TextFormat("Catapult: %dG", TowerTypeGetCosts(TOWER_TYPE_CATAPULT)),
878 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
879 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_CATAPULT);
880 }
881
882 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
883 {
884 level->placementContextMenuStatus = -1;
885 }
886 }
887
888 void DrawLevelBuildingState(Level *level)
889 {
890 // when the context menu is not active, we update the placement position
891 if (level->placementContextMenuStatus == 0)
892 {
893 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
894 float hitDistance = ray.position.y / -ray.direction.y;
895 float hitX = ray.direction.x * hitDistance + ray.position.x;
896 float hitY = ray.direction.z * hitDistance + ray.position.z;
897 level->placementX = (int)floorf(hitX + 0.5f);
898 level->placementY = (int)floorf(hitY + 0.5f);
899 }
900
901 // the currently hovered/selected tower
902 Tower *tower = TowerGetAt(level->placementX, level->placementY);
903 // show the range of the tower when hovering/selecting it
904 TowerUpdateAllRangeFade(tower, 0.0f);
905
906 BeginMode3D(level->camera);
907 DrawLevelGround(level);
908 PathFindingMapUpdate(0, 0);
909 TowerDrawAll();
910 EnemyDraw();
911 ProjectileDraw();
912 ParticleDraw();
913 DrawEnemyPaths(level);
914
915 guiState.isBlocked = 0;
916
917 // Hover rectangle, when the mouse is over the map
918 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
919 if (isHovering)
920 {
921 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
922 }
923
924 EndMode3D();
925
926 TowerDrawAllHealthBars(level->camera);
927
928 DrawTitle("Building phase");
929
930 // Draw the context menu when the context menu is active
931 if (level->placementContextMenuStatus >= 1)
932 {
933 float maxHitpoints = 0.0f;
934 float hp = 0.0f;
935 float damageFactor = 0.0f;
936 int32_t sellValue = 0;
937
938 if (tower)
939 {
940 maxHitpoints = TowerGetMaxHealth(tower);
941 hp = maxHitpoints - tower->damage;
942 damageFactor = 1.0f - tower->damage / maxHitpoints;
943 sellValue = (int32_t) ceilf(TowerTypeGetCosts(tower->towerType) * 0.5f * damageFactor);
944 }
945
946 ContextMenuItem menu[12] = {0};
947 int menuCount = 0;
948 int menuIndex = 0;
949 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
950 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
951
952 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
953 {
954 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
955 }
956 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
957 {
958 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
959 int costs = totalLevel * 4;
960 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
961 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
962 TowerTypeGetName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
963 int buttonMenuIndex = menuIndex;
964 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
965 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
966 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
967 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
968 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
969 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
970
971 // check if buttons should be disabled
972 if (isMaxLevel || level->playerGold < costs)
973 {
974 for (int i = buttonMenuIndex; i < menuCount; i++)
975 {
976 menu[i].buttonState.isDisabled = 1;
977 }
978 }
979
980 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
981 {
982 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
983 }
984 }
985 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
986 {
987 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", TowerTypeGetName(tower->towerType)), 0.5f);
988 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
989 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
990 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
991 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
992 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
993 {
994 level->placementContextMenuStatus = -1;
995 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
996 }
997 }
998 }
999
1000 // Activate the context menu when the mouse is clicked and the context menu is not active
1001 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
1002 {
1003 level->placementContextMenuStatus += 1;
1004 }
1005
1006 if (level->placementContextMenuStatus == 0)
1007 {
1008 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1009 {
1010 level->nextState = LEVEL_STATE_RESET;
1011 }
1012
1013 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1014 {
1015 level->nextState = LEVEL_STATE_BATTLE;
1016 }
1017
1018 }
1019 }
1020
1021 void InitBattleStateConditions(Level *level)
1022 {
1023 level->state = LEVEL_STATE_BATTLE;
1024 level->nextState = LEVEL_STATE_NONE;
1025 level->waveEndTimer = 0.0f;
1026 for (int i = 0; i < 10; i++)
1027 {
1028 EnemyWave *wave = &level->waves[i];
1029 wave->spawned = 0;
1030 wave->timeToSpawnNext = wave->delay;
1031 }
1032 }
1033
1034 void DrawLevelBattleState(Level *level)
1035 {
1036 BeginMode3D(level->camera);
1037 DrawLevelGround(level);
1038 TowerUpdateAllRangeFade(0, 0.0f);
1039 TowerDrawAll();
1040 EnemyDraw();
1041 ProjectileDraw();
1042 ParticleDraw();
1043 guiState.isBlocked = 0;
1044 EndMode3D();
1045
1046 EnemyDrawHealthbars(level->camera);
1047 TowerDrawAllHealthBars(level->camera);
1048
1049 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1050 {
1051 level->nextState = LEVEL_STATE_RESET;
1052 }
1053
1054 int maxCount = 0;
1055 int remainingCount = 0;
1056 for (int i = 0; i < 10; i++)
1057 {
1058 EnemyWave *wave = &level->waves[i];
1059 if (wave->wave != level->currentWave)
1060 {
1061 continue;
1062 }
1063 maxCount += wave->count;
1064 remainingCount += wave->count - wave->spawned;
1065 }
1066 int aliveCount = EnemyCount();
1067 remainingCount += aliveCount;
1068
1069 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1070 DrawTitle(text);
1071 }
1072
1073 void DrawLevel(Level *level)
1074 {
1075 switch (level->state)
1076 {
1077 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1078 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1079 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1080 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1081 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1082 default: break;
1083 }
1084
1085 DrawLevelHud(level);
1086 }
1087
1088 void UpdateLevel(Level *level)
1089 {
1090 if (level->state == LEVEL_STATE_BATTLE)
1091 {
1092 int activeWaves = 0;
1093 for (int i = 0; i < 10; i++)
1094 {
1095 EnemyWave *wave = &level->waves[i];
1096 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1097 {
1098 continue;
1099 }
1100 activeWaves++;
1101 wave->timeToSpawnNext -= gameTime.deltaTime;
1102 if (wave->timeToSpawnNext <= 0.0f)
1103 {
1104 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1105 if (enemy)
1106 {
1107 wave->timeToSpawnNext = wave->interval;
1108 wave->spawned++;
1109 }
1110 }
1111 }
1112 if (TowerGetByType(TOWER_TYPE_BASE) == 0) {
1113 level->waveEndTimer += gameTime.deltaTime;
1114 if (level->waveEndTimer >= 2.0f)
1115 {
1116 level->nextState = LEVEL_STATE_LOST_WAVE;
1117 }
1118 }
1119 else if (activeWaves == 0 && EnemyCount() == 0)
1120 {
1121 level->waveEndTimer += gameTime.deltaTime;
1122 if (level->waveEndTimer >= 2.0f)
1123 {
1124 level->nextState = LEVEL_STATE_WON_WAVE;
1125 }
1126 }
1127 }
1128
1129 PathFindingMapUpdate(0, 0);
1130 EnemyUpdate();
1131 TowerUpdate();
1132 ProjectileUpdate();
1133 ParticleUpdate();
1134
1135 if (level->nextState == LEVEL_STATE_RESET)
1136 {
1137 InitLevel(level);
1138 }
1139
1140 if (level->nextState == LEVEL_STATE_BATTLE)
1141 {
1142 InitBattleStateConditions(level);
1143 }
1144
1145 if (level->nextState == LEVEL_STATE_WON_WAVE)
1146 {
1147 level->currentWave++;
1148 level->state = LEVEL_STATE_WON_WAVE;
1149 }
1150
1151 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1152 {
1153 level->state = LEVEL_STATE_LOST_WAVE;
1154 }
1155
1156 if (level->nextState == LEVEL_STATE_BUILDING)
1157 {
1158 level->state = LEVEL_STATE_BUILDING;
1159 level->placementContextMenuStatus = 0;
1160 }
1161
1162 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1163 {
1164 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1165 level->placementTransitionPosition = (Vector2){
1166 level->placementX, level->placementY};
1167 // initialize the spring to the current position
1168 level->placementTowerSpring = (PhysicsPoint){
1169 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1170 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1171 };
1172 level->placementPhase = PLACEMENT_PHASE_STARTING;
1173 level->placementTimer = 0.0f;
1174 }
1175
1176 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1177 {
1178 // make something of this later
1179 InitLevel(level);
1180 }
1181
1182 level->nextState = LEVEL_STATE_NONE;
1183 }
1184
1185 float nextSpawnTime = 0.0f;
1186
1187 void LoadConfig()
1188 {
1189 char *config = LoadFileText("data/level.txt");
1190 if (!config)
1191 {
1192 TraceLog(LOG_ERROR, "Failed to load level config");
1193 return;
1194 }
1195
1196 ParsedGameData gameData = {0};
1197 if (ParseGameData(&gameData, config))
1198 {
1199 for (int i = 0; i < 8; i++)
1200 {
1201 EnemyClassConfig *enemyClassConfig = &gameData.enemyClasses[i];
1202 if (enemyClassConfig->health > 0.0f)
1203 {
1204 enemyClassConfigs[i] = *enemyClassConfig;
1205 }
1206 }
1207
1208 for (int i = 0; i < 32; i++)
1209 {
1210 Level *level = &gameData.levels[i];
1211 if (level->initialGold > 0)
1212 {
1213 loadedLevels[i] = *level;
1214 }
1215 }
1216
1217 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
1218 {
1219 TowerTypeConfig *towerTypeConfig = &gameData.towerTypes[i];
1220 if (towerTypeConfig->maxHealth > 0)
1221 {
1222 TowerTypeSetData(i, towerTypeConfig);
1223 }
1224 }
1225 }
1226
1227 UnloadFileText(config);
1228 }
1229
1230 void InitGame()
1231 {
1232 TowerInit();
1233 EnemyInit();
1234 ProjectileInit();
1235 ParticleInit();
1236 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1237
1238 currentLevel = loadedLevels[0].initialGold > 0 ? loadedLevels : levels;
1239 InitLevel(currentLevel);
1240 }
1241
1242 //# Immediate GUI functions
1243
1244 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1245 {
1246 const float healthBarHeight = 6.0f;
1247 const float healthBarOffset = 15.0f;
1248 const float inset = 2.0f;
1249 const float innerWidth = healthBarWidth - inset * 2;
1250 const float innerHeight = healthBarHeight - inset * 2;
1251
1252 Vector2 screenPos = GetWorldToScreen(position, camera);
1253 screenPos = Vector2Add(screenPos, screenOffset);
1254 float centerX = screenPos.x - healthBarWidth * 0.5f;
1255 float topY = screenPos.y - healthBarOffset;
1256 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1257 float healthWidth = innerWidth * healthRatio;
1258 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1259 }
1260
1261 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1262 {
1263 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1264
1265 DrawTextEx(gameFontNormal, text, (Vector2){
1266 x + (width - textSize.x) * alignX,
1267 y + (height - textSize.y) * alignY
1268 }, gameFontNormal.baseSize, 1, textColor);
1269 }
1270
1271 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1272 {
1273 Rectangle bounds = {x, y, width, height};
1274 int isPressed = 0;
1275 int isSelected = state && state->isSelected;
1276 int isDisabled = state && state->isDisabled;
1277 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1278 {
1279 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1280 {
1281 isPressed = 1;
1282 }
1283 guiState.isBlocked = 1;
1284 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1285 bounds, Vector2Zero(), 0, WHITE);
1286 }
1287 else
1288 {
1289 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1290 bounds, Vector2Zero(), 0, WHITE);
1291 }
1292 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1293 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1294 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1295 return isPressed;
1296 }
1297
1298 //# Main game loop
1299
1300 void GameUpdate()
1301 {
1302 UpdateLevel(currentLevel);
1303 }
1304
1305 int main(void)
1306 {
1307 int screenWidth, screenHeight;
1308 GetPreferredSize(&screenWidth, &screenHeight);
1309 InitWindow(screenWidth, screenHeight, "Tower defense");
1310 float gamespeed = 1.0f;
1311 int frameRate = 30;
1312 SetTargetFPS(30);
1313
1314 LoadAssets();
1315 InitGame();
1316
1317 float pause = 1.0f;
1318
1319 while (!WindowShouldClose())
1320 {
1321 if (IsPaused()) {
1322 // canvas is not visible in browser - do nothing
1323 continue;
1324 }
1325
1326 if (IsKeyPressed(KEY_F))
1327 {
1328 frameRate = (frameRate + 5) % 30;
1329 frameRate = frameRate < 10 ? 10 : frameRate;
1330 SetTargetFPS(frameRate);
1331 }
1332
1333 if (IsKeyPressed(KEY_T))
1334 {
1335 gamespeed += 0.1f;
1336 if (gamespeed > 1.05f) gamespeed = 0.1f;
1337 }
1338
1339 if (IsKeyPressed(KEY_P))
1340 {
1341 pause = pause > 0.5f ? 0.0f : 1.0f;
1342 }
1343
1344 float dt = GetFrameTime() * gamespeed * pause;
1345 // cap maximum delta time to 0.1 seconds to prevent large time steps
1346 if (dt > 0.1f) dt = 0.1f;
1347 gameTime.time += dt;
1348 gameTime.deltaTime = dt;
1349 gameTime.frameCount += 1;
1350
1351 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1352 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1353
1354 BeginDrawing();
1355 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1356
1357 GameUpdate();
1358 DrawLevel(currentLevel);
1359
1360 if (gamespeed != 1.0f)
1361 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1362 EndDrawing();
1363
1364 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1365 }
1366
1367 CloseWindow();
1368
1369 return 0;
1370 }
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 typedef struct ParsedGameData
286 {
287 const char *parseError;
288 Level levels[32];
289 int lastLevelIndex;
290 TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
291 EnemyClassConfig enemyClasses[8];
292 } ParsedGameData;
293
294 //# Function declarations
295 int ParseGameData(ParsedGameData *gameData, const char *input);
296 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
297 int EnemyAddDamageRange(Vector2 position, float range, float damage);
298 int EnemyAddDamage(Enemy *enemy, float damage);
299
300 //# Enemy functions
301 void EnemyInit();
302 void EnemyDraw();
303 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
304 void EnemyUpdate();
305 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
306 float EnemyGetMaxHealth(Enemy *enemy);
307 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
308 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
309 EnemyId EnemyGetId(Enemy *enemy);
310 Enemy *EnemyTryResolve(EnemyId enemyId);
311 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
312 int EnemyAddDamage(Enemy *enemy, float damage);
313 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
314 int EnemyCount();
315 void EnemyDrawHealthbars(Camera3D camera);
316
317 //# Tower functions
318 const char *TowerTypeGetName(uint8_t towerType);
319 int TowerTypeGetCosts(uint8_t towerType);
320 void TowerTypeSetData(uint8_t towerType, TowerTypeConfig *data);
321 void TowerInit();
322 void TowerUpdate();
323 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget);
324 void TowerDrawAll();
325 void TowerDrawAllHealthBars(Camera3D camera);
326 void TowerDrawModel(Tower *tower);
327 void TowerDrawRange(Tower *tower, float alpha);
328 Tower *TowerGetByIndex(int index);
329 Tower *TowerGetByType(uint8_t towerType);
330 Tower *TowerGetAt(int16_t x, int16_t y);
331 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
332 float TowerGetMaxHealth(Tower *tower);
333 float TowerGetRange(Tower *tower);
334
335 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
336
337 //# Particles
338 void ParticleInit();
339 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
340 void ParticleUpdate();
341 void ParticleDraw();
342
343 //# Projectiles
344 void ProjectileInit();
345 void ProjectileDraw();
346 void ProjectileUpdate();
347 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
348
349 //# Pathfinding map
350 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
351 float PathFindingGetDistance(int mapX, int mapY);
352 Vector2 PathFindingGetGradient(Vector3 world);
353 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
354 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
355 void PathFindingMapDraw();
356
357 //# UI
358 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
359
360 //# Level
361 void DrawLevelGround(Level *level);
362 void DrawEnemyPath(Level *level, Color arrowColor);
363
364 //# variables
365 extern Level *currentLevel;
366 extern Enemy enemies[ENEMY_MAX_COUNT];
367 extern int enemyCount;
368 extern EnemyClassConfig enemyClassConfigs[];
369
370 extern GUIState guiState;
371 extern GameTime gameTime;
372 extern Tower towers[TOWER_MAX_COUNT];
373 extern int towerCount;
374
375 extern Texture2D palette, spriteSheet;
376
377 #endif
1 #include "td_main.h"
2 #include <raylib.h>
3 #include <stdio.h>
4 #include <string.h>
5
6 typedef struct ParserState
7 {
8 char *input;
9 int position;
10 char nextToken[256];
11 } ParserState;
12
13 int ParserStateGetLineNumber(ParserState *state)
14 {
15 int lineNumber = 1;
16 for (int i = 0; i < state->position; i++)
17 {
18 if (state->input[i] == '\n')
19 {
20 lineNumber++;
21 }
22 }
23 return lineNumber;
24 }
25
26 void ParserStateSkipWhiteSpaces(ParserState *state)
27 {
28 char *input = state->input;
29 int pos = state->position;
30 int skipped = 1;
31 while (skipped)
32 {
33 skipped = 0;
34 if (input[pos] == '-' && input[pos + 1] == '-')
35 {
36 skipped = 1;
37 // skip comments
38 while (input[pos] != 0 && input[pos] != '\n')
39 {
40 pos++;
41 }
42 }
43
44 // skip white spaces and ignore colons
45 while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
46 {
47 skipped = 1;
48 pos++;
49 }
50
51 // repeat until no more white spaces or comments
52 }
53 state->position = pos;
54 }
55
56 int ParserStateReadNextToken(ParserState *state)
57 {
58 ParserStateSkipWhiteSpaces(state);
59
60 int i = 0, pos = state->position;
61 char *input = state->input;
62
63 // read token
64 while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
65 {
66 state->nextToken[i] = input[pos];
67 pos++;
68 i++;
69 }
70 state->position = pos;
71
72 if (i == 0 || i == 256)
73 {
74 state->nextToken[0] = 0;
75 return 0;
76 }
77 // terminate the token
78 state->nextToken[i] = 0;
79 return 1;
80 }
81
82 int ParserStateReadNextInt(ParserState *state, int *value)
83 {
84 if (!ParserStateReadNextToken(state))
85 {
86 return 0;
87 }
88 // check if the token is a valid integer
89 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
90 for (int i = isSigned; state->nextToken[i] != 0; i++)
91 {
92 if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
93 {
94 return 0;
95 }
96 }
97 *value = TextToInteger(state->nextToken);
98 return 1;
99 }
100
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103 if (!ParserStateReadNextToken(state))
104 {
105 return 0;
106 }
107 // check if the token is a valid float number
108 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109 int hasDot = 0;
110 for (int i = isSigned; state->nextToken[i] != 0; i++)
111 {
112 if (state->nextToken[i] == '.')
113 {
114 if (hasDot)
115 {
116 return 0;
117 }
118 hasDot = 1;
119 }
120 else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121 {
122 return 0;
123 }
124 }
125
126 *value = TextToFloat(state->nextToken);
127 return 1;
128 }
129
130 typedef enum TryReadResult
131 {
132 TryReadResult_NoMatch,
133 TryReadResult_Error,
134 TryReadResult_Success
135 } TryReadResult;
136
137 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
138 {
139 gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
140 return TryReadResult_Error;
141 }
142
143 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
144 {
145 if (!TextIsEqual(state->nextToken, key))
146 {
147 return TryReadResult_NoMatch;
148 }
149
150 if (!ParserStateReadNextInt(state, value))
151 {
152 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
153 }
154
155 // range test, if minRange == maxRange, we don't check the range
156 if (minRange != maxRange && (*value < minRange || *value > maxRange))
157 {
158 return ParseGameDataError(gameData, state, TextFormat(
159 "Invalid value range for %s, range is [%d, %d], value is %d",
160 key, minRange, maxRange, *value));
161 }
162
163 return TryReadResult_Success;
164 }
165
166 TryReadResult ParseGameDataTryReadKeyUInt8(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *value, uint8_t minRange, uint8_t maxRange)
167 {
168 int intValue = *value;
169 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
170 *value = (uint8_t) intValue;
171 return result;
172 }
173
174 TryReadResult ParseGameDataTryReadKeyInt16(ParsedGameData *gameData, ParserState *state, const char *key, int16_t *value, int16_t minRange, int16_t maxRange)
175 {
176 int intValue = *value;
177 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
178 *value = (int16_t) intValue;
179 return result;
180 }
181
182 TryReadResult ParseGameDataTryReadKeyUInt16(ParsedGameData *gameData, ParserState *state, const char *key, uint16_t *value, uint16_t minRange, uint16_t maxRange)
183 {
184 int intValue = *value;
185 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
186 *value = (uint16_t) intValue;
187 return result;
188 }
189
190 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key,
191 Vector2 *vector, Vector2 minRange, Vector2 maxRange)
192 {
193 if (!TextIsEqual(state->nextToken, key))
194 {
195 return TryReadResult_NoMatch;
196 }
197
198 ParserState start = *state;
199 int x = 0, y = 0;
200 int minXRange = (int)minRange.x, maxXRange = (int)maxRange.x;
201 int minYRange = (int)minRange.y, maxYRange = (int)maxRange.y;
202
203 if (!ParserStateReadNextInt(state, &x))
204 {
205 // use start position to report the error for this KEY
206 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
207 }
208
209 // range test, if minRange == maxRange, we don't check the range
210 if (minXRange != maxXRange && (x < minXRange || x > maxXRange))
211 {
212 // use current position to report the error for x value
213 return ParseGameDataError(gameData, state, TextFormat(
214 "Invalid value x range for %s, range is [%d, %d], value is %d",
215 key, minXRange, maxXRange, x));
216 }
217
218 if (!ParserStateReadNextInt(state, &y))
219 {
220 // use start position to report the error for this KEY
221 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key));
222 }
223
224 if (minYRange != maxYRange && (y < minYRange || y > maxYRange))
225 {
226 // use current position to report the error for y value
227 return ParseGameDataError(gameData, state, TextFormat(
228 "Invalid value y range for %s, range is [%d, %d], value is %d",
229 key, minYRange, maxYRange, y));
230 }
231
232 vector->x = (float)x;
233 vector->y = (float)y;
234
235 return TryReadResult_Success;
236 }
237
238 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
239 {
240 if (!TextIsEqual(state->nextToken, key))
241 {
242 return TryReadResult_NoMatch;
243 }
244
245 if (!ParserStateReadNextFloat(state, value))
246 {
247 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
248 }
249
250 // range test, if minRange == maxRange, we don't check the range
251 if (minRange != maxRange && (*value < minRange || *value > maxRange))
252 {
253 return ParseGameDataError(gameData, state, TextFormat(
254 "Invalid value range for %s, range is [%f, %f], value is %f",
255 key, minRange, maxRange, *value));
256 }
257
258 return TryReadResult_Success;
259 }
260
261 // The enumNames is a null-terminated array of strings that represent the enum values
262 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues)
263 {
264 if (!TextIsEqual(state->nextToken, key))
265 {
266 return TryReadResult_NoMatch;
267 }
268
269 if (!ParserStateReadNextToken(state))
270 {
271 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key));
272 }
273
274 for (int i = 0; enumNames[i] != 0; i++)
275 {
276 if (TextIsEqual(state->nextToken, enumNames[i]))
277 {
278 *value = enumValues[i];
279 return TryReadResult_Success;
280 }
281 }
282
283 return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key));
284 }
285
286 TryReadResult ParseGameDataTryReadKeyEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId, uint8_t minRange, uint8_t maxRange)
287 {
288 int enemyClassId = *enemyTypeId;
289 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId,
290 (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0},
291 (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS});
292 if (minRange != maxRange)
293 {
294 enemyClassId = enemyClassId < minRange ? minRange : enemyClassId;
295 enemyClassId = enemyClassId > maxRange ? maxRange : enemyClassId;
296 }
297 *enemyTypeId = (uint8_t) enemyClassId;
298 return result;
299 }
300
301 TryReadResult ParseGameDataTryReadKeyTowerTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
302 {
303 int towerType = *towerTypeId;
304 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType,
305 (const char *[]){"TOWER_TYPE_BASE", "TOWER_TYPE_ARCHER", "TOWER_TYPE_BALLISTA", "TOWER_TYPE_CATAPULT", "TOWER_TYPE_WALL", 0},
306 (int[]){TOWER_TYPE_BASE, TOWER_TYPE_ARCHER, TOWER_TYPE_BALLISTA, TOWER_TYPE_CATAPULT, TOWER_TYPE_WALL});
307 if (minRange != maxRange)
308 {
309 towerType = towerType < minRange ? minRange : towerType;
310 towerType = towerType > maxRange ? maxRange : towerType;
311 }
312 *towerTypeId = (uint8_t) towerType;
313 return result;
314 }
315
316 TryReadResult ParseGameDataTryReadKeyProjectileTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
317 {
318 int towerType = *towerTypeId;
319 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType,
320 (const char *[]){"PROJECTILE_TYPE_ARROW", "PROJECTILE_TYPE_BALLISTA", "PROJECTILE_TYPE_CATAPULT", 0},
321 (int[]){PROJECTILE_TYPE_ARROW, PROJECTILE_TYPE_BALLISTA, PROJECTILE_TYPE_CATAPULT});
322 if (minRange != maxRange)
323 {
324 towerType = towerType < minRange ? minRange : towerType;
325 towerType = towerType > maxRange ? maxRange : towerType;
326 }
327 *towerTypeId = (uint8_t) towerType;
328 return result;
329 }
330
331
332 //----------------------------------------------------------------
333 //# Defines for compact struct field parsing
334 // A FIELDS(GENERATEr) is to be defined that will be called for each field of the struct
335 // See implementations below for how this is used
336 #define GENERATE_READFIELD_SWITCH(owner, name, type, min, max)\
337 switch (ParseGameDataTryReadKey##type(gameData, state, #name, &owner->name, min, max))\
338 {\
339 case TryReadResult_NoMatch: break;\
340 case TryReadResult_Success:\
341 if (name##Initialized) {\
342 return ParseGameDataError(gameData, state, #name " already initialized");\
343 }\
344 name##Initialized = 1;\
345 continue;\
346 case TryReadResult_Error: return TryReadResult_Error;\
347 }
348 #define GENERATE_READFIELD_SWITCH_OPTIONAL(owner, name, type, def, min, max)\
349 GENERATE_READFIELD_SWITCH(owner, name, type, min, max)
350 #define GENERATE_FIELD_INIT_DECLARATIONS(owner, name, type, min, max) int name##Initialized = 0;
351 #define GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL(owner, name, type, def, min, max) int name##Initialized = 0; owner->name = def;
352 #define GENERATE_FIELD_INIT_CHECK(owner, name, type, min, max) \
353 if (!name##Initialized) { \
354 return ParseGameDataError(gameData, state, #name " not initialized"); \
355 }
356 #define GENERATE_FIELD_INIT_CHECK_OPTIONAL(owner, name, type, def, min, max)
357
358 #define GENERATE_FIELD_PARSING \
359 FIELDS(GENERATE_FIELD_INIT_DECLARATIONS, GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL)\
360 while (1)\
361 {\
362 ParserState prevState = *state;\
363 \
364 if (!ParserStateReadNextToken(state))\
365 {\
366 /* end of file */\
367 break;\
368 }\
369 FIELDS(GENERATE_READFIELD_SWITCH, GENERATE_READFIELD_SWITCH_OPTIONAL)\
370 /* no match, return to previous state and break */\
371 *state = prevState;\
372 break;\
373 } \
374 FIELDS(GENERATE_FIELD_INIT_CHECK, GENERATE_FIELD_INIT_CHECK_OPTIONAL)\
375
376 // END OF DEFINES
377
378 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state)
379 {
380 if (!TextIsEqual(state->nextToken, "Wave"))
381 {
382 return TryReadResult_NoMatch;
383 }
384
385 Level *level = &gameData->levels[gameData->lastLevelIndex];
386 EnemyWave *wave = 0;
387 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
388 {
389 if (level->waves[i].count == 0)
390 {
391 wave = &level->waves[i];
392 break;
393 }
394 }
395
396 if (wave == 0)
397 {
398 return ParseGameDataError(gameData, state,
399 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT));
400 }
401
402 #define FIELDS(MANDATORY, OPTIONAL) \
403 MANDATORY(wave, wave, UInt8, 0, ENEMY_MAX_WAVE_COUNT - 1) \
404 MANDATORY(wave, count, UInt16, 1, 1000) \
405 MANDATORY(wave, delay, Float, 0.0f, 1000.0f) \
406 MANDATORY(wave, interval, Float, 0.0f, 1000.0f) \
407 MANDATORY(wave, spawnPosition, IntVec2, ((Vector2){-10.0f, -10.0f}), ((Vector2){10.0f, 10.0f})) \
408 MANDATORY(wave, enemyType, EnemyTypeId, 0, 0)
409
410 GENERATE_FIELD_PARSING
411 #undef FIELDS
412
413 return TryReadResult_Success;
414 }
415
416 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state)
417 {
418 uint8_t enemyClassId;
419
420 switch (ParseGameDataTryReadKeyEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId, 0, 7))
421 {
422 case TryReadResult_Success: break;
423 case TryReadResult_NoMatch: return TryReadResult_NoMatch;
424 case TryReadResult_Error: return TryReadResult_Error;
425 }
426
427 EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId];
428
429 #define FIELDS(MANDATORY, OPTIONAL) \
430 MANDATORY(enemyClass, speed, Float, 0.1f, 1000.0f) \
431 MANDATORY(enemyClass, health, Float, 1, 1000000) \
432 MANDATORY(enemyClass, radius, Float, 0.0f, 10.0f) \
433 MANDATORY(enemyClass, requiredContactTime, Float, 0.0f, 1000.0f) \
434 MANDATORY(enemyClass, maxAcceleration, Float, 0.0f, 1000.0f) \
435 MANDATORY(enemyClass, explosionDamage, Float, 0.0f, 1000.0f) \
436 MANDATORY(enemyClass, explosionRange, Float, 0.0f, 1000.0f) \
437 MANDATORY(enemyClass, explosionPushbackPower, Float, 0.0f, 1000.0f) \
438 MANDATORY(enemyClass, goldValue, Int, 1, 1000000)
439
440 GENERATE_FIELD_PARSING
441 #undef FIELDS
442
443 return TryReadResult_Success;
444 }
445
446 TryReadResult ParseGameDataTryReadTowerTypeConfigSection(ParsedGameData *gameData, ParserState *state)
447 {
448 uint8_t towerTypeId;
449
450 switch (ParseGameDataTryReadKeyTowerTypeId(gameData, state, "TowerTypeConfig", &towerTypeId, 0, TOWER_TYPE_COUNT - 1))
451 {
452 case TryReadResult_Success: break;
453 case TryReadResult_NoMatch: return TryReadResult_NoMatch;
454 case TryReadResult_Error: return TryReadResult_Error;
455 }
456
457 TowerTypeConfig *towerType = &gameData->towerTypes[towerTypeId];
458 HitEffectConfig *hitEffect = &towerType->hitEffect;
459
460 #define FIELDS(MANDATORY, OPTIONAL) \
461 MANDATORY(towerType, maxHealth, UInt16, 0, 0) \
462 OPTIONAL(towerType, cooldown, Float, 0, 0.0f, 1000.0f) \
463 OPTIONAL(towerType, maxUpgradeCooldown, Float, 0, 0.0f, 1000.0f) \
464 OPTIONAL(towerType, range, Float, 0, 0.0f, 50.0f) \
465 OPTIONAL(towerType, maxUpgradeRange, Float, 0, 0.0f, 50.0f) \
466 OPTIONAL(towerType, projectileSpeed, Float, 0, 0.0f, 100.0f) \
467 OPTIONAL(towerType, cost, UInt8, 0, 0, 255) \
468 OPTIONAL(towerType, projectileType, ProjectileTypeId, 0, 0, 32)\
469 OPTIONAL(hitEffect, damage, Float, 0, 0, 100000.0f) \
470 OPTIONAL(hitEffect, maxUpgradeDamage, Float, 0, 0, 100000.0f) \
471 OPTIONAL(hitEffect, areaDamageRadius, Float, 0, 0, 100000.0f) \
472 OPTIONAL(hitEffect, pushbackPowerDistance, Float, 0, 0, 100000.0f)
473
474 GENERATE_FIELD_PARSING
475 #undef FIELDS
476
477 return TryReadResult_Success;
478 }
479
480 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state)
481 {
482 int levelId;
483 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31);
484 if (result != TryReadResult_Success)
485 {
486 return result;
487 }
488
489 gameData->lastLevelIndex = levelId;
490 Level *level = &gameData->levels[levelId];
491
492 // since we require the initialGold to be initialized with at least 1, we can use it as a flag
493 // to detect if the level was already initialized
494 if (level->initialGold != 0)
495 {
496 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId));
497 }
498
499 int initialGoldInitialized = 0;
500
501 while (1)
502 {
503 // try to read the next token and if we don't know how to GENERATE it,
504 // we rewind and return
505 ParserState prevState = *state;
506
507 if (!ParserStateReadNextToken(state))
508 {
509 // end of file
510 break;
511 }
512
513 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000))
514 {
515 case TryReadResult_Success:
516 if (initialGoldInitialized)
517 {
518 return ParseGameDataError(gameData, state, "initialGold already initialized");
519 }
520 initialGoldInitialized = 1;
521 continue;
522 case TryReadResult_Error: return TryReadResult_Error;
523 case TryReadResult_NoMatch: break;
524 }
525
526 switch (ParseGameDataTryReadWaveSection(gameData, state))
527 {
528 case TryReadResult_Success: continue;
529 case TryReadResult_NoMatch: break;
530 case TryReadResult_Error: return TryReadResult_Error;
531 }
532
533 // no match, return to previous state and break
534 *state = prevState;
535 break;
536 }
537
538 if (!initialGoldInitialized)
539 {
540 return ParseGameDataError(gameData, state, "initialGold not initialized");
541 }
542
543 return TryReadResult_Success;
544 }
545
546 int ParseGameData(ParsedGameData *gameData, const char *input)
547 {
548 ParserState state = (ParserState){(char *)input, 0, {0}};
549 *gameData = (ParsedGameData){0};
550 gameData->lastLevelIndex = -1;
551
552 while (ParserStateReadNextToken(&state))
553 {
554 switch (ParseGameDataTryReadLevelSection(gameData, &state))
555 {
556 case TryReadResult_Success: continue;
557 case TryReadResult_Error: return 0;
558 case TryReadResult_NoMatch: break;
559 }
560
561 switch (ParseGameDataTryReadEnemyClassSection(gameData, &state))
562 {
563 case TryReadResult_Success: continue;
564 case TryReadResult_Error: return 0;
565 case TryReadResult_NoMatch: break;
566 }
567
568 switch (ParseGameDataTryReadTowerTypeConfigSection(gameData, &state))
569 {
570 case TryReadResult_Success: continue;
571 case TryReadResult_Error: return 0;
572 case TryReadResult_NoMatch: break;
573 }
574
575 // any other token is considered an error
576 ParseGameDataError(gameData, &state, TextFormat("Unexpected token: %s", state.nextToken));
577 return 0;
578 }
579
580 return 1;
581 }
582
583 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; }
584
585 int RunParseTests()
586 {
587 int passedCount = 0, failedCount = 0;
588 ParsedGameData gameData;
589 const char *input;
590
591 input ="Level 7\n initialGold 100\nLevel 2 initialGold 200";
592 gameData = (ParsedGameData) {0};
593 EXPECT(ParseGameData(&gameData, input) == 1, "Failed to parse level section");
594 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2");
595 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100");
596 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200");
597
598 input ="Level 392\n";
599 gameData = (ParsedGameData) {0};
600 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
601 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error");
602 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1");
603
604 input ="Level 3\n initialGold -34";
605 gameData = (ParsedGameData) {0};
606 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
607 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error");
608 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1");
609
610 input ="Level 3\n initialGold 2\n initialGold 3";
611 gameData = (ParsedGameData) {0};
612 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
613 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1");
614
615 input ="Level 3";
616 gameData = (ParsedGameData) {0};
617 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
618
619 input ="Level 7\n initialGold 100\nLevel 7 initialGold 200";
620 gameData = (ParsedGameData) {0};
621 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
622 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1");
623
624 input =
625 "Level 7\n initialGold 100\n"
626 "Wave\n"
627 "count 1 wave 2\n"
628 "interval 0.5\n"
629 "delay 1.0\n"
630 "spawnPosition -3 4\n"
631 "enemyType: ENEMY_TYPE_SHIELD"
632 ;
633 gameData = (ParsedGameData) {0};
634 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing level/wave section");
635 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1");
636 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2");
637 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5");
638 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0");
639 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3");
640 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4");
641 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD");
642
643 // for every entry in the wave section, we want to verify that if that value is
644 // missing, the parser will produce an error. We can do that by commenting out each
645 // line individually in a loop - just replacing the two leading spaces with two dashes
646 const char *testString =
647 "Level 7 initialGold 100\n"
648 "Wave\n"
649 " count 1\n"
650 " wave 2\n"
651 " interval 0.5\n"
652 " delay 1.0\n"
653 " spawnPosition 3 -4\n"
654 " enemyType: ENEMY_TYPE_SHIELD";
655 for (int i = 0; testString[i]; i++)
656 {
657 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ')
658 {
659 char copy[1024];
660 strcpy(copy, testString);
661 // commentify!
662 copy[i + 1] = '-';
663 copy[i + 2] = '-';
664 gameData = (ParsedGameData) {0};
665 EXPECT(ParseGameData(&gameData, copy) == 0, "Expected to fail parsing level/wave section");
666 }
667 }
668
669 // test wave section missing data / incorrect data
670
671 input =
672 "Level 7\n initialGold 100\n"
673 "Wave\n"
674 "count 1 wave 2\n"
675 "interval 0.5\n"
676 "delay 1.0\n"
677 "spawnPosition -3\n" // missing y
678 "enemyType: ENEMY_TYPE_SHIELD"
679 ;
680 gameData = (ParsedGameData) {0};
681 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level/wave section");
682 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7");
683
684 input =
685 "Level 7\n initialGold 100\n"
686 "Wave\n"
687 "count 1.0 wave 2\n"
688 "interval 0.5\n"
689 "delay 1.0\n"
690 "spawnPosition -3\n" // missing y
691 "enemyType: ENEMY_TYPE_SHIELD"
692 ;
693 gameData = (ParsedGameData) {0};
694 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level/wave section");
695 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3");
696
697 // enemy class config parsing tests
698 input =
699 "EnemyClass ENEMY_TYPE_MINION\n"
700 " health: 10.0\n"
701 " speed: 0.6\n"
702 " radius: 0.25\n"
703 " maxAcceleration: 1.0\n"
704 " explosionDamage: 1.0\n"
705 " requiredContactTime: 0.5\n"
706 " explosionRange: 1.0\n"
707 " explosionPushbackPower: 0.25\n"
708 " goldValue: 1\n"
709 ;
710 gameData = (ParsedGameData) {0};
711 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing enemy class section");
712 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0");
713 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6");
714 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25");
715 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0");
716 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0");
717 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5");
718 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0");
719 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25");
720 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1");
721
722 testString =
723 "EnemyClass ENEMY_TYPE_MINION\n"
724 " health: 10.0\n"
725 " speed: 0.6\n"
726 " radius: 0.25\n"
727 " maxAcceleration: 1.0\n"
728 " explosionDamage: 1.0\n"
729 " requiredContactTime: 0.5\n"
730 " explosionRange: 1.0\n"
731 " explosionPushbackPower: 0.25\n"
732 " goldValue: 1\n";
733 for (int i = 0; testString[i]; i++)
734 {
735 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ')
736 {
737 char copy[1024];
738 strcpy(copy, testString);
739 // commentify!
740 copy[i + 1] = '-';
741 copy[i + 2] = '-';
742 gameData = (ParsedGameData) {0};
743 EXPECT(ParseGameData(&gameData, copy) == 0, "Expected to fail parsing EnemyClass section");
744 }
745 }
746
747 input =
748 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
749 " cooldown: 0.5\n"
750 " maxUpgradeCooldown: 0.25\n"
751 " range: 3\n"
752 " maxUpgradeRange: 5\n"
753 " projectileSpeed: 4.0\n"
754 " cost: 5\n"
755 " maxHealth: 10\n"
756 " projectileType: PROJECTILE_TYPE_ARROW\n"
757 " damage: 0.5\n"
758 " maxUpgradeDamage: 1.5\n"
759 " areaDamageRadius: 0\n"
760 " pushbackPowerDistance: 0\n"
761 ;
762 gameData = (ParsedGameData) {0};
763 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing tower type section");
764 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5");
765 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeCooldown == 0.25f, "Expected maxUpgradeCooldown to be 0.25");
766 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].range == 3.0f, "Expected range to be 3.0");
767 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeRange == 5.0f, "Expected maxUpgradeRange to be 5.0");
768 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileSpeed == 4.0f, "Expected projectileSpeed to be 4.0");
769 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 5, "Expected cost to be 5");
770 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
771 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileType == PROJECTILE_TYPE_ARROW, "Expected projectileType to be PROJECTILE_TYPE_ARROW");
772 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.damage == 0.5f, "Expected damage to be 0.5");
773 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.maxUpgradeDamage == 1.5f, "Expected maxUpgradeDamage to be 1.5");
774 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.areaDamageRadius == 0.0f, "Expected areaDamageRadius to be 0.0");
775 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.pushbackPowerDistance == 0.0f, "Expected pushbackPowerDistance to be 0.0");
776
777 input =
778 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
779 " maxHealth: 10\n"
780 " cooldown: 0.5\n"
781 ;
782 gameData = (ParsedGameData) {0};
783 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing tower type section");
784 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5");
785 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
786 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0");
787
788
789 input =
790 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
791 " cooldown: 0.5\n"
792 ;
793 gameData = (ParsedGameData) {0};
794 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing tower type section");
795 EXPECT(TextFindIndex(gameData.parseError, "maxHealth not initialized") >= 0, "Expected to find maxHealth not initialized");
796
797 input =
798 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
799 " maxHealth: 10\n"
800 " foobar: 0.5\n"
801 ;
802 gameData = (ParsedGameData) {0};
803 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing tower type section");
804 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
805 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0");
806
807 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount);
808
809 return failedCount;
810 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .name = "Castle",
7 .maxHealth = 10,
8 },
9 [TOWER_TYPE_ARCHER] = {
10 .name = "Archer",
11 .cooldown = 0.5f,
12 .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 void TowerTypeSetData(uint8_t towerType, TowerTypeConfig *data)
208 {
209 towerTypeConfigs[towerType] = *data;
210 }
211
212 Tower *TowerGetAt(int16_t x, int16_t y)
213 {
214 for (int i = 0; i < towerCount; i++)
215 {
216 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
217 {
218 return &towers[i];
219 }
220 }
221 return 0;
222 }
223
224 Tower *TowerGetByIndex(int index)
225 {
226 if (index < 0 || index >= towerCount)
227 {
228 return 0;
229 }
230 return &towers[index];
231 }
232
233 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
234 {
235 if (towerCount >= TOWER_MAX_COUNT)
236 {
237 return 0;
238 }
239
240 Tower *tower = TowerGetAt(x, y);
241 if (tower)
242 {
243 return 0;
244 }
245
246 tower = &towers[towerCount++];
247 *tower = (Tower){
248 .x = x,
249 .y = y,
250 .towerType = towerType,
251 .cooldown = 0.0f,
252 .damage = 0.0f,
253 };
254 return tower;
255 }
256
257 Tower *TowerGetByType(uint8_t towerType)
258 {
259 for (int i = 0; i < towerCount; i++)
260 {
261 if (towers[i].towerType == towerType)
262 {
263 return &towers[i];
264 }
265 }
266 return 0;
267 }
268
269 const char *TowerTypeGetName(uint8_t towerType)
270 {
271 return towerTypeConfigs[towerType].name;
272 }
273
274 int TowerTypeGetCosts(uint8_t towerType)
275 {
276 return towerTypeConfigs[towerType].cost;
277 }
278
279 float TowerGetMaxHealth(Tower *tower)
280 {
281 return towerTypeConfigs[tower->towerType].maxHealth;
282 }
283
284 float TowerGetRange(Tower *tower)
285 {
286 float range = towerTypeConfigs[tower->towerType].range;
287 float maxUpgradeRange = towerTypeConfigs[tower->towerType].maxUpgradeRange;
288 if (tower->upgradeState.range > 0)
289 {
290 range = Lerp(range, maxUpgradeRange, tower->upgradeState.range / (float)TOWER_MAX_STAGE);
291 }
292 return range;
293 }
294
295 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget)
296 {
297 // animate fade in and fade out of range drawing using framerate independent lerp
298 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime);
299 for (int i = 0; i < TOWER_MAX_COUNT; i++)
300 {
301 Tower *fadingTower = TowerGetByIndex(i);
302 if (!fadingTower)
303 {
304 break;
305 }
306 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget;
307 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp);
308 }
309 }
310
311 void TowerDrawRange(Tower *tower, float alpha)
312 {
313 Color ringColor = (Color){255, 200, 100, 255};
314 const int rings = 4;
315 const float radiusOffset = 0.5f;
316 const float animationSpeed = 2.0f;
317 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
318 float radius = TowerGetRange(tower);
319 // base circle
320 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, radius, (Vector3){1, 0, 0}, 90,
321 Fade(ringColor, alpha));
322
323 for (int i = 1; i < rings; i++)
324 {
325 float t = ((float)i + animation) / (float)rings;
326 float r = Lerp(radius, radius - radiusOffset, t * t);
327 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
328 if (i == 1)
329 {
330 // fade out the outermost ring
331 a = animation;
332 }
333 a *= alpha;
334
335 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, r, (Vector3){1, 0, 0}, 90,
336 Fade(ringColor, a));
337 }
338 }
339
340 void TowerDrawModel(Tower *tower)
341 {
342 if (tower->towerType == TOWER_TYPE_NONE)
343 {
344 return;
345 }
346
347 if (tower->drawRangeAlpha > 2.0f/256.0f)
348 {
349 TowerDrawRange(tower, tower->drawRangeAlpha);
350 }
351
352 switch (tower->towerType)
353 {
354 case TOWER_TYPE_ARCHER:
355 {
356 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower->x, 0.0f, tower->y}, currentLevel->camera);
357 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower->lastTargetPosition.x, 0.0f, tower->lastTargetPosition.y}, currentLevel->camera);
358 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
359 DrawSpriteUnit(archerUnit, (Vector3){tower->x, 1.0f, tower->y}, 0, screenPosTarget.x > screenPosTower.x,
360 tower->cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
361 }
362 break;
363 case TOWER_TYPE_BALLISTA:
364 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, BROWN);
365 break;
366 case TOWER_TYPE_CATAPULT:
367 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
368 break;
369 default:
370 if (towerModels[tower->towerType].materials)
371 {
372 DrawModel(towerModels[tower->towerType], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
373 } else {
374 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
375 }
376 break;
377 }
378 }
379
380 void TowerDrawAll()
381 {
382 for (int i = 0; i < towerCount; i++)
383 {
384 TowerDrawModel(&towers[i]);
385 }
386 }
387
388 void TowerUpdate()
389 {
390 for (int i = 0; i < towerCount; i++)
391 {
392 Tower *tower = &towers[i];
393 switch (tower->towerType)
394 {
395 case TOWER_TYPE_CATAPULT:
396 case TOWER_TYPE_BALLISTA:
397 case TOWER_TYPE_ARCHER:
398 TowerGunUpdate(tower);
399 break;
400 }
401 }
402 }
403
404 void TowerDrawAllHealthBars(Camera3D camera)
405 {
406 for (int i = 0; i < towerCount; i++)
407 {
408 Tower *tower = &towers[i];
409 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
410 {
411 continue;
412 }
413
414 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
415 float maxHealth = TowerGetMaxHealth(tower);
416 float health = maxHealth - tower->damage;
417 float healthRatio = health / maxHealth;
418
419 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
420 }
421 }
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 new level configuration specifies only one wave with a few more enemies than before.
The next step is to allow reloading the configuration file without restarting the game in the web version - the desktop version allows this by default since it loads the config when resetting the level.
The way I want this to work is that the when the game starts in the web version, it creates a text area with the content of the configuration file. The player can modify the configuration and press a button to reload the game with the new configuration.
I will hide from now on the files that are not important for the part, so the files shown here are just a selection.
A new file is the html-edit.js file, which handles the editing part in the browser:
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 #ifdef PLATFORM_WEB
8 #include <emscripten/emscripten.h>
9 #else
10 #define EMSCRIPTEN_KEEPALIVE
11 #endif
12
13 //# Variables
14 Font gameFontNormal = {0};
15 GUIState guiState = {0};
16 GameTime gameTime = {
17 .fixedDeltaTime = 1.0f / 60.0f,
18 };
19
20 Model floorTileAModel = {0};
21 Model floorTileBModel = {0};
22 Model treeModel[2] = {0};
23 Model firTreeModel[2] = {0};
24 Model rockModels[5] = {0};
25 Model grassPatchModel[1] = {0};
26
27 Model pathArrowModel = {0};
28 Model greenArrowModel = {0};
29
30 Texture2D palette, spriteSheet;
31
32 NPatchInfo uiPanelPatch = {
33 .layout = NPATCH_NINE_PATCH,
34 .source = {145, 1, 46, 46},
35 .top = 18, .bottom = 18,
36 .left = 16, .right = 16
37 };
38 NPatchInfo uiButtonNormal = {
39 .layout = NPATCH_NINE_PATCH,
40 .source = {193, 1, 32, 20},
41 .top = 7, .bottom = 7,
42 .left = 10, .right = 10
43 };
44 NPatchInfo uiButtonDisabled = {
45 .layout = NPATCH_NINE_PATCH,
46 .source = {193, 22, 32, 20},
47 .top = 7, .bottom = 7,
48 .left = 10, .right = 10
49 };
50 NPatchInfo uiButtonHovered = {
51 .layout = NPATCH_NINE_PATCH,
52 .source = {193, 43, 32, 20},
53 .top = 7, .bottom = 7,
54 .left = 10, .right = 10
55 };
56 NPatchInfo uiButtonPressed = {
57 .layout = NPATCH_NINE_PATCH,
58 .source = {193, 64, 32, 20},
59 .top = 7, .bottom = 7,
60 .left = 10, .right = 10
61 };
62 Rectangle uiDiamondMarker = {145, 48, 15, 15};
63
64 Level loadedLevels[32] = {0};
65 Level levels[] = {
66 [0] = {
67 .state = LEVEL_STATE_BUILDING,
68 .initialGold = 500,
69 .waves[0] = {
70 .enemyType = ENEMY_TYPE_SHIELD,
71 .wave = 0,
72 .count = 1,
73 .interval = 2.5f,
74 .delay = 1.0f,
75 .spawnPosition = {2, 6},
76 },
77 .waves[1] = {
78 .enemyType = ENEMY_TYPE_RUNNER,
79 .wave = 0,
80 .count = 5,
81 .interval = 0.5f,
82 .delay = 1.0f,
83 .spawnPosition = {-2, 6},
84 },
85 .waves[2] = {
86 .enemyType = ENEMY_TYPE_SHIELD,
87 .wave = 1,
88 .count = 20,
89 .interval = 1.5f,
90 .delay = 1.0f,
91 .spawnPosition = {0, 6},
92 },
93 .waves[3] = {
94 .enemyType = ENEMY_TYPE_MINION,
95 .wave = 2,
96 .count = 30,
97 .interval = 1.2f,
98 .delay = 1.0f,
99 .spawnPosition = {2, 6},
100 },
101 .waves[4] = {
102 .enemyType = ENEMY_TYPE_BOSS,
103 .wave = 2,
104 .count = 2,
105 .interval = 5.0f,
106 .delay = 2.0f,
107 .spawnPosition = {-2, 4},
108 }
109 },
110 };
111
112 Level *currentLevel = levels;
113
114 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
115 void LoadConfig();
116
117 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
118 {
119 int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
120 int panelWidth = textWidth + 40;
121 int posX = anchorX - panelWidth * alignX;
122 int textOffset = 20;
123 DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
124 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
125 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
126 }
127
128 void DrawTitle(const char *text)
129 {
130 DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
131 }
132
133 //# Game
134
135 static Model LoadGLBModel(char *filename)
136 {
137 Model model = LoadModel(TextFormat("data/%s.glb",filename));
138 for (int i = 0; i < model.materialCount; i++)
139 {
140 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
141 }
142 return model;
143 }
144
145 void LoadAssets()
146 {
147 // load a sprite sheet that contains all units
148 spriteSheet = LoadTexture("data/spritesheet.png");
149 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
150
151 // we'll use a palette texture to colorize the all buildings and environment art
152 palette = LoadTexture("data/palette.png");
153 // The texture uses gradients on very small space, so we'll enable bilinear filtering
154 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
155
156 gameFontNormal = LoadFont("data/alagard.png");
157
158 floorTileAModel = LoadGLBModel("floor-tile-a");
159 floorTileBModel = LoadGLBModel("floor-tile-b");
160 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
161 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
162 firTreeModel[0] = LoadGLBModel("firtree-1-a");
163 firTreeModel[1] = LoadGLBModel("firtree-1-b");
164 rockModels[0] = LoadGLBModel("rock-1");
165 rockModels[1] = LoadGLBModel("rock-2");
166 rockModels[2] = LoadGLBModel("rock-3");
167 rockModels[3] = LoadGLBModel("rock-4");
168 rockModels[4] = LoadGLBModel("rock-5");
169 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
170
171 pathArrowModel = LoadGLBModel("direction-arrow-x");
172 greenArrowModel = LoadGLBModel("green-arrow");
173 }
174
175 void InitLevel(Level *level)
176 {
177 LoadConfig();
178
179 level->seed = (int)(GetTime() * 100.0f);
180
181 TowerInit();
182 EnemyInit();
183 ProjectileInit();
184 ParticleInit();
185 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
186
187 level->placementMode = 0;
188 level->state = LEVEL_STATE_BUILDING;
189 level->nextState = LEVEL_STATE_NONE;
190 level->playerGold = level->initialGold;
191 level->currentWave = 0;
192 level->placementX = -1;
193 level->placementY = 0;
194
195 Camera *camera = &level->camera;
196 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
197 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
198 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
199 camera->fovy = 11.5f;
200 camera->projection = CAMERA_ORTHOGRAPHIC;
201 }
202
203 void DrawLevelHud(Level *level)
204 {
205 const char *text = TextFormat("Gold: %d", level->playerGold);
206 DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
207 }
208
209 void DrawLevelReportLostWave(Level *level)
210 {
211 BeginMode3D(level->camera);
212 DrawLevelGround(level);
213 TowerUpdateAllRangeFade(0, 0.0f);
214 TowerDrawAll();
215 EnemyDraw();
216 ProjectileDraw();
217 ParticleDraw();
218 guiState.isBlocked = 0;
219 EndMode3D();
220
221 TowerDrawAllHealthBars(level->camera);
222
223 DrawTitle("Wave lost");
224
225 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
226 {
227 level->nextState = LEVEL_STATE_RESET;
228 }
229 }
230
231 int HasLevelNextWave(Level *level)
232 {
233 for (int i = 0; i < 10; i++)
234 {
235 EnemyWave *wave = &level->waves[i];
236 if (wave->wave == level->currentWave)
237 {
238 return 1;
239 }
240 }
241 return 0;
242 }
243
244 void DrawLevelReportWonWave(Level *level)
245 {
246 BeginMode3D(level->camera);
247 DrawLevelGround(level);
248 TowerUpdateAllRangeFade(0, 0.0f);
249 TowerDrawAll();
250 EnemyDraw();
251 ProjectileDraw();
252 ParticleDraw();
253 guiState.isBlocked = 0;
254 EndMode3D();
255
256 TowerDrawAllHealthBars(level->camera);
257
258 DrawTitle("Wave won");
259
260
261 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
262 {
263 level->nextState = LEVEL_STATE_RESET;
264 }
265
266 if (HasLevelNextWave(level))
267 {
268 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
269 {
270 level->nextState = LEVEL_STATE_BUILDING;
271 }
272 }
273 else {
274 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
275 {
276 level->nextState = LEVEL_STATE_WON_LEVEL;
277 }
278 }
279 }
280
281 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
282 {
283 static ButtonState buttonStates[8] = {0};
284 int cost = TowerTypeGetCosts(towerType);
285 const char *text = TextFormat("%s: %d", name, cost);
286 buttonStates[towerType].isSelected = level->placementMode == towerType;
287 buttonStates[towerType].isDisabled = level->playerGold < cost;
288 if (Button(text, x, y, width, height, &buttonStates[towerType]))
289 {
290 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
291 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
292 return 1;
293 }
294 return 0;
295 }
296
297 float GetRandomFloat(float min, float max)
298 {
299 int random = GetRandomValue(0, 0xfffffff);
300 return ((float)random / (float)0xfffffff) * (max - min) + min;
301 }
302
303 void DrawLevelGround(Level *level)
304 {
305 // draw checkerboard ground pattern
306 for (int x = -5; x <= 5; x += 1)
307 {
308 for (int y = -5; y <= 5; y += 1)
309 {
310 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
311 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
312 }
313 }
314
315 int oldSeed = GetRandomValue(0, 0xfffffff);
316 SetRandomSeed(level->seed);
317 // increase probability for trees via duplicated entries
318 Model borderModels[64];
319 int maxRockCount = GetRandomValue(2, 6);
320 int maxTreeCount = GetRandomValue(10, 20);
321 int maxFirTreeCount = GetRandomValue(5, 10);
322 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
323 int grassPatchCount = GetRandomValue(5, 30);
324
325 int modelCount = 0;
326 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
327 {
328 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
329 }
330 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
331 {
332 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
333 }
334 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
335 {
336 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
337 }
338 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
339 {
340 borderModels[modelCount++] = grassPatchModel[0];
341 }
342
343 // draw some objects around the border of the map
344 Vector3 up = {0, 1, 0};
345 // a pseudo random number generator to get the same result every time
346 const float wiggle = 0.75f;
347 const int layerCount = 3;
348 for (int layer = 0; layer <= layerCount; layer++)
349 {
350 int layerPos = 6 + layer;
351 Model *selectedModels = borderModels;
352 int selectedModelCount = modelCount;
353 if (layer == 0)
354 {
355 selectedModels = grassPatchModel;
356 selectedModelCount = 1;
357 }
358 for (int x = -6 - layer; x <= 6 + layer; x += 1)
359 {
360 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
361 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
362 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
363 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
364 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
365 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
366 }
367
368 for (int z = -5 - layer; z <= 5 + layer; z += 1)
369 {
370 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
371 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
372 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
373 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
374 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
375 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
376 }
377 }
378
379 SetRandomSeed(oldSeed);
380 }
381
382 void DrawEnemyPath(Level *level, Color arrowColor)
383 {
384 const int castleX = 0, castleY = 0;
385 const int maxWaypointCount = 200;
386 const float timeStep = 1.0f;
387 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
388
389 // we start with a time offset to simulate the path,
390 // this way the arrows are animated in a forward moving direction
391 // The time is wrapped around the time step to get a smooth animation
392 float timeOffset = fmodf(GetTime(), timeStep);
393
394 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
395 {
396 EnemyWave *wave = &level->waves[i];
397 if (wave->wave != level->currentWave)
398 {
399 continue;
400 }
401
402 // use this dummy enemy to simulate the path
403 Enemy dummy = {
404 .enemyType = ENEMY_TYPE_MINION,
405 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
406 .nextX = wave->spawnPosition.x,
407 .nextY = wave->spawnPosition.y,
408 .currentX = wave->spawnPosition.x,
409 .currentY = wave->spawnPosition.y,
410 };
411
412 float deltaTime = timeOffset;
413 for (int j = 0; j < maxWaypointCount; j++)
414 {
415 int waypointPassedCount = 0;
416 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
417 // after the initial variable starting offset, we use a fixed time step
418 deltaTime = timeStep;
419 dummy.simPosition = pos;
420
421 // Update the dummy's position just like we do in the regular enemy update loop
422 for (int k = 0; k < waypointPassedCount; k++)
423 {
424 dummy.currentX = dummy.nextX;
425 dummy.currentY = dummy.nextY;
426 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
427 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
428 {
429 break;
430 }
431 }
432 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
433 {
434 break;
435 }
436
437 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
438 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
439 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
440 }
441 }
442 }
443
444 void DrawEnemyPaths(Level *level)
445 {
446 // disable depth testing for the path arrows
447 // flush the 3D batch to draw the arrows on top of everything
448 rlDrawRenderBatchActive();
449 rlDisableDepthTest();
450 DrawEnemyPath(level, (Color){64, 64, 64, 160});
451
452 rlDrawRenderBatchActive();
453 rlEnableDepthTest();
454 DrawEnemyPath(level, WHITE);
455 }
456
457 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
458 {
459 float dt = gameTime.fixedDeltaTime;
460 // smooth transition for the placement position using exponential decay
461 const float lambda = 15.0f;
462 float factor = 1.0f - expf(-lambda * dt);
463
464 float damping = 0.5f;
465 float springStiffness = 300.0f;
466 float springDecay = 95.0f;
467 float minHeight = 0.35f;
468
469 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
470 {
471 damping = 1.0f;
472 springDecay = 90.0f;
473 springStiffness = 100.0f;
474 minHeight = 0.70f;
475 }
476
477 for (int i = 0; i < gameTime.fixedStepCount; i++)
478 {
479 level->placementTransitionPosition =
480 Vector2Lerp(
481 level->placementTransitionPosition,
482 (Vector2){mapX, mapY}, factor);
483
484 // draw the spring position for debugging the spring simulation
485 // first step: stiff spring, no simulation
486 Vector3 worldPlacementPosition = (Vector3){
487 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
488 Vector3 springTargetPosition = (Vector3){
489 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
490 // consider the current velocity to predict the future position in order to dampen
491 // the spring simulation. Longer prediction times will result in more damping
492 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
493 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
494 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
495 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
496 // decay velocity of the upright forcing spring
497 // This force acts like a 2nd spring that pulls the tip upright into the air above the
498 // base position
499 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
500 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
501
502 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
503 // we use a simple spring model with a rest length of 1.0f
504 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
505 float springLength = Vector3Length(springDelta);
506 float springForce = (springLength - 1.0f) * springStiffness;
507 Vector3 springForceVector = Vector3Normalize(springDelta);
508 springForceVector = Vector3Scale(springForceVector, springForce);
509 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
510 Vector3Scale(springForceVector, dt));
511
512 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
513 Vector3Scale(level->placementTowerSpring.velocity, dt));
514 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
515 {
516 level->placementTowerSpring.velocity.y *= -1.0f;
517 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
518 }
519 }
520 }
521
522 void DrawLevelBuildingPlacementState(Level *level)
523 {
524 const float placementDuration = 0.5f;
525
526 level->placementTimer += gameTime.deltaTime;
527 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
528 {
529 level->placementPhase = PLACEMENT_PHASE_MOVING;
530 level->placementTimer = 0.0f;
531 }
532
533 BeginMode3D(level->camera);
534 DrawLevelGround(level);
535
536 int blockedCellCount = 0;
537 Vector2 blockedCells[1];
538 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
539 float planeDistance = ray.position.y / -ray.direction.y;
540 float planeX = ray.direction.x * planeDistance + ray.position.x;
541 float planeY = ray.direction.z * planeDistance + ray.position.z;
542 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
543 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
544 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
545 level->placementMode && !guiState.isBlocked &&
546 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
547 {
548 level->placementX = mapX;
549 level->placementY = mapY;
550 }
551 else
552 {
553 mapX = level->placementX;
554 mapY = level->placementY;
555 }
556 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
557 PathFindingMapUpdate(blockedCellCount, blockedCells);
558
559 TowerUpdateAllRangeFade(0, 0.0f);
560 TowerDrawAll();
561 EnemyDraw();
562 ProjectileDraw();
563 ParticleDraw();
564 DrawEnemyPaths(level);
565
566 // let the tower float up and down. Consider this height in the spring simulation as well
567 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
568
569 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
570 {
571 // The bouncing spring needs a bit of outro time to look nice and complete.
572 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
573 // and the last 1/3rd is the outro physics (bouncing)
574 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
575 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
576 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
577 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
578 towerFloatHeight = linearBlendHeight + parabola;
579 }
580
581 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
582
583 rlPushMatrix();
584 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
585
586 // calculate x and z rotation to align the model with the spring
587 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
588 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
589 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
590 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
591 float springLength = Vector3Length(towerUp);
592 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
593 float towerSquash = 1.0f / towerStretch;
594
595 Tower dummy = {
596 .towerType = level->placementMode,
597 };
598
599 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
600 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
601 {
602 rangeAlpha = 1.0f - rangeAlpha;
603 }
604 else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
605 {
606 rangeAlpha = 1.0f;
607 }
608
609 TowerDrawRange(&dummy, rangeAlpha);
610
611 rlPushMatrix();
612 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
613
614 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
615 rlScalef(towerSquash, towerStretch, towerSquash);
616 TowerDrawModel(&dummy);
617 rlPopMatrix();
618
619
620 // draw a shadow for the tower
621 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
622 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
623 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
624
625
626 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
627 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
628 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
629 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
630
631 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
632 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
633 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
634 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
635 rlPopMatrix();
636
637 guiState.isBlocked = 0;
638
639 EndMode3D();
640
641 TowerDrawAllHealthBars(level->camera);
642
643 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
644 {
645 if (level->placementTimer > placementDuration)
646 {
647 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
648 // testing repairing
649 tower->damage = 2.5f;
650 level->playerGold -= TowerTypeGetCosts(level->placementMode);
651 level->nextState = LEVEL_STATE_BUILDING;
652 level->placementMode = TOWER_TYPE_NONE;
653 }
654 }
655 else
656 {
657 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
658 {
659 level->nextState = LEVEL_STATE_BUILDING;
660 level->placementMode = TOWER_TYPE_NONE;
661 TraceLog(LOG_INFO, "Cancel building");
662 }
663
664 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
665 {
666 level->placementPhase = PLACEMENT_PHASE_PLACING;
667 level->placementTimer = 0.0f;
668 }
669 }
670 }
671
672 enum ContextMenuType
673 {
674 CONTEXT_MENU_TYPE_MAIN,
675 CONTEXT_MENU_TYPE_SELL_CONFIRM,
676 CONTEXT_MENU_TYPE_UPGRADE,
677 };
678
679 enum UpgradeType
680 {
681 UPGRADE_TYPE_SPEED,
682 UPGRADE_TYPE_DAMAGE,
683 UPGRADE_TYPE_RANGE,
684 };
685
686 typedef struct ContextMenuArgs
687 {
688 void *data;
689 uint8_t uint8;
690 int32_t int32;
691 Tower *tower;
692 } ContextMenuArgs;
693
694 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
695 {
696 uint8_t towerType = data->uint8;
697 level->placementMode = towerType;
698 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
699 return 1;
700 }
701
702 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
703 {
704 Tower *tower = data->tower;
705 int gold = data->int32;
706 level->playerGold += gold;
707 tower->towerType = TOWER_TYPE_NONE;
708 return 1;
709 }
710
711 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
712 {
713 return 1;
714 }
715
716 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
717 {
718 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
719 return 0;
720 }
721
722 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
723 {
724 Tower *tower = data->tower;
725 switch (data->uint8)
726 {
727 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
728 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
729 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
730 }
731 level->playerGold -= data->int32;
732 return 0;
733 }
734
735 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
736 {
737 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
738 return 0;
739 }
740
741 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
742 {
743 Tower *tower = data->tower;
744 if (level->playerGold >= 1)
745 {
746 level->playerGold -= 1;
747 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
748 }
749 return tower->damage == 0.0f;
750 }
751
752 typedef struct ContextMenuItem
753 {
754 uint8_t index;
755 char text[24];
756 float alignX;
757 int (*action)(Level*, ContextMenuArgs*);
758 void *data;
759 ContextMenuArgs args;
760 ButtonState buttonState;
761 } ContextMenuItem;
762
763 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
764 {
765 ContextMenuItem item = {.index = index, .alignX = alignX};
766 strncpy(item.text, text, 23);
767 item.text[23] = 0;
768 return item;
769 }
770
771 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
772 {
773 ContextMenuItem item = {.index = index, .action = action, .args = args};
774 strncpy(item.text, text, 23);
775 item.text[23] = 0;
776 return item;
777 }
778
779 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
780 {
781 const int itemHeight = 28;
782 const int itemSpacing = 1;
783 const int padding = 8;
784 int itemCount = 0;
785 for (int i = 0; menus[i].text[0]; i++)
786 {
787 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
788 }
789
790 Rectangle contextMenu = {0, 0, width,
791 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
792
793 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
794 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
795
796 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
797 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
798 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
799 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
800
801 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
802 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
803 const int itemX = contextMenu.x + itemSpacing;
804 const int itemWidth = contextMenu.width - itemSpacing * 2;
805 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
806 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
807 int status = 0;
808 for (int i = 0; menus[i].text[0]; i++)
809 {
810 if (menus[i].action)
811 {
812 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
813 {
814 status = menus[i].action(level, &menus[i].args);
815 }
816 }
817 else
818 {
819 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
820 }
821 }
822
823 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
824 {
825 return 1;
826 }
827
828 return status;
829 }
830
831 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
832 {
833 ContextMenuItem menu[12] = {0};
834 int menuCount = 0;
835 int menuIndex = 0;
836 if (tower)
837 {
838
839 if (tower) {
840 menu[menuCount++] = ContextMenuItemText(menuIndex++, TowerTypeGetName(tower->towerType), 0.5f);
841 }
842
843 // two texts, same line
844 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
845 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
846
847 if (tower->towerType != TOWER_TYPE_BASE)
848 {
849 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
850 (ContextMenuArgs){.tower = tower});
851 }
852
853 if (tower->towerType != TOWER_TYPE_BASE)
854 {
855
856 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
857 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
858 }
859 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
860 {
861 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
862 (ContextMenuArgs){.tower = tower});
863 }
864 }
865 else
866 {
867 menu[menuCount] = ContextMenuItemButton(menuIndex++,
868 TextFormat("Wall: %dG", TowerTypeGetCosts(TOWER_TYPE_WALL)),
869 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
870 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_WALL);
871
872 menu[menuCount] = ContextMenuItemButton(menuIndex++,
873 TextFormat("Archer: %dG", TowerTypeGetCosts(TOWER_TYPE_ARCHER)),
874 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
875 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_ARCHER);
876
877 menu[menuCount] = ContextMenuItemButton(menuIndex++,
878 TextFormat("Ballista: %dG", TowerTypeGetCosts(TOWER_TYPE_BALLISTA)),
879 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
880 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_BALLISTA);
881
882 menu[menuCount] = ContextMenuItemButton(menuIndex++,
883 TextFormat("Catapult: %dG", TowerTypeGetCosts(TOWER_TYPE_CATAPULT)),
884 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
885 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_CATAPULT);
886 }
887
888 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
889 {
890 level->placementContextMenuStatus = -1;
891 }
892 }
893
894 void DrawLevelBuildingState(Level *level)
895 {
896 // when the context menu is not active, we update the placement position
897 if (level->placementContextMenuStatus == 0)
898 {
899 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
900 float hitDistance = ray.position.y / -ray.direction.y;
901 float hitX = ray.direction.x * hitDistance + ray.position.x;
902 float hitY = ray.direction.z * hitDistance + ray.position.z;
903 level->placementX = (int)floorf(hitX + 0.5f);
904 level->placementY = (int)floorf(hitY + 0.5f);
905 }
906
907 // the currently hovered/selected tower
908 Tower *tower = TowerGetAt(level->placementX, level->placementY);
909 // show the range of the tower when hovering/selecting it
910 TowerUpdateAllRangeFade(tower, 0.0f);
911
912 BeginMode3D(level->camera);
913 DrawLevelGround(level);
914 PathFindingMapUpdate(0, 0);
915 TowerDrawAll();
916 EnemyDraw();
917 ProjectileDraw();
918 ParticleDraw();
919 DrawEnemyPaths(level);
920
921 guiState.isBlocked = 0;
922
923 // Hover rectangle, when the mouse is over the map
924 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
925 if (isHovering)
926 {
927 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
928 }
929
930 EndMode3D();
931
932 TowerDrawAllHealthBars(level->camera);
933
934 DrawTitle("Building phase");
935
936 // Draw the context menu when the context menu is active
937 if (level->placementContextMenuStatus >= 1)
938 {
939 float maxHitpoints = 0.0f;
940 float hp = 0.0f;
941 float damageFactor = 0.0f;
942 int32_t sellValue = 0;
943
944 if (tower)
945 {
946 maxHitpoints = TowerGetMaxHealth(tower);
947 hp = maxHitpoints - tower->damage;
948 damageFactor = 1.0f - tower->damage / maxHitpoints;
949 sellValue = (int32_t) ceilf(TowerTypeGetCosts(tower->towerType) * 0.5f * damageFactor);
950 }
951
952 ContextMenuItem menu[12] = {0};
953 int menuCount = 0;
954 int menuIndex = 0;
955 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
956 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
957
958 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
959 {
960 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
961 }
962 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
963 {
964 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
965 int costs = totalLevel * 4;
966 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
967 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
968 TowerTypeGetName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
969 int buttonMenuIndex = menuIndex;
970 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
971 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
972 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
973 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
974 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
975 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
976
977 // check if buttons should be disabled
978 if (isMaxLevel || level->playerGold < costs)
979 {
980 for (int i = buttonMenuIndex; i < menuCount; i++)
981 {
982 menu[i].buttonState.isDisabled = 1;
983 }
984 }
985
986 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
987 {
988 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
989 }
990 }
991 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
992 {
993 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", TowerTypeGetName(tower->towerType)), 0.5f);
994 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
995 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
996 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
997 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
998 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
999 {
1000 level->placementContextMenuStatus = -1;
1001 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
1002 }
1003 }
1004 }
1005
1006 // Activate the context menu when the mouse is clicked and the context menu is not active
1007 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
1008 {
1009 level->placementContextMenuStatus += 1;
1010 }
1011
1012 if (level->placementContextMenuStatus == 0)
1013 {
1014 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1015 {
1016 level->nextState = LEVEL_STATE_RESET;
1017 }
1018
1019 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1020 {
1021 level->nextState = LEVEL_STATE_BATTLE;
1022 }
1023
1024 }
1025 }
1026
1027 void InitBattleStateConditions(Level *level)
1028 {
1029 level->state = LEVEL_STATE_BATTLE;
1030 level->nextState = LEVEL_STATE_NONE;
1031 level->waveEndTimer = 0.0f;
1032 for (int i = 0; i < 10; i++)
1033 {
1034 EnemyWave *wave = &level->waves[i];
1035 wave->spawned = 0;
1036 wave->timeToSpawnNext = wave->delay;
1037 }
1038 }
1039
1040 void DrawLevelBattleState(Level *level)
1041 {
1042 BeginMode3D(level->camera);
1043 DrawLevelGround(level);
1044 TowerUpdateAllRangeFade(0, 0.0f);
1045 TowerDrawAll();
1046 EnemyDraw();
1047 ProjectileDraw();
1048 ParticleDraw();
1049 guiState.isBlocked = 0;
1050 EndMode3D();
1051
1052 EnemyDrawHealthbars(level->camera);
1053 TowerDrawAllHealthBars(level->camera);
1054
1055 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1056 {
1057 level->nextState = LEVEL_STATE_RESET;
1058 }
1059
1060 int maxCount = 0;
1061 int remainingCount = 0;
1062 for (int i = 0; i < 10; i++)
1063 {
1064 EnemyWave *wave = &level->waves[i];
1065 if (wave->wave != level->currentWave)
1066 {
1067 continue;
1068 }
1069 maxCount += wave->count;
1070 remainingCount += wave->count - wave->spawned;
1071 }
1072 int aliveCount = EnemyCount();
1073 remainingCount += aliveCount;
1074
1075 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1076 DrawTitle(text);
1077 }
1078
1079 void DrawLevel(Level *level)
1080 {
1081 switch (level->state)
1082 {
1083 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1084 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1085 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1086 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1087 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1088 default: break;
1089 }
1090
1091 DrawLevelHud(level);
1092 }
1093
1094 EMSCRIPTEN_KEEPALIVE
1095 void RequestReload()
1096 {
1097 currentLevel->nextState = LEVEL_STATE_RESET;
1098 }
1099
1100 void UpdateLevel(Level *level)
1101 {
1102 if (level->state == LEVEL_STATE_BATTLE)
1103 {
1104 int activeWaves = 0;
1105 for (int i = 0; i < 10; i++)
1106 {
1107 EnemyWave *wave = &level->waves[i];
1108 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1109 {
1110 continue;
1111 }
1112 activeWaves++;
1113 wave->timeToSpawnNext -= gameTime.deltaTime;
1114 if (wave->timeToSpawnNext <= 0.0f)
1115 {
1116 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1117 if (enemy)
1118 {
1119 wave->timeToSpawnNext = wave->interval;
1120 wave->spawned++;
1121 }
1122 }
1123 }
1124 if (TowerGetByType(TOWER_TYPE_BASE) == 0) {
1125 level->waveEndTimer += gameTime.deltaTime;
1126 if (level->waveEndTimer >= 2.0f)
1127 {
1128 level->nextState = LEVEL_STATE_LOST_WAVE;
1129 }
1130 }
1131 else if (activeWaves == 0 && EnemyCount() == 0)
1132 {
1133 level->waveEndTimer += gameTime.deltaTime;
1134 if (level->waveEndTimer >= 2.0f)
1135 {
1136 level->nextState = LEVEL_STATE_WON_WAVE;
1137 }
1138 }
1139 }
1140
1141 PathFindingMapUpdate(0, 0);
1142 EnemyUpdate();
1143 TowerUpdate();
1144 ProjectileUpdate();
1145 ParticleUpdate();
1146
1147 if (level->nextState == LEVEL_STATE_RESET)
1148 {
1149 InitLevel(level);
1150 }
1151
1152 if (level->nextState == LEVEL_STATE_BATTLE)
1153 {
1154 InitBattleStateConditions(level);
1155 }
1156
1157 if (level->nextState == LEVEL_STATE_WON_WAVE)
1158 {
1159 level->currentWave++;
1160 level->state = LEVEL_STATE_WON_WAVE;
1161 }
1162
1163 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1164 {
1165 level->state = LEVEL_STATE_LOST_WAVE;
1166 }
1167
1168 if (level->nextState == LEVEL_STATE_BUILDING)
1169 {
1170 level->state = LEVEL_STATE_BUILDING;
1171 level->placementContextMenuStatus = 0;
1172 }
1173
1174 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1175 {
1176 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1177 level->placementTransitionPosition = (Vector2){
1178 level->placementX, level->placementY};
1179 // initialize the spring to the current position
1180 level->placementTowerSpring = (PhysicsPoint){
1181 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1182 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1183 };
1184 level->placementPhase = PLACEMENT_PHASE_STARTING;
1185 level->placementTimer = 0.0f;
1186 }
1187
1188 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1189 {
1190 // make something of this later
1191 InitLevel(level);
1192 }
1193
1194 level->nextState = LEVEL_STATE_NONE;
1195 }
1196
1197 float nextSpawnTime = 0.0f;
1198
1199 void LoadConfig()
1200 {
1201 char *config = LoadFileText("data/level.txt");
1202 if (!config)
1203 {
1204 TraceLog(LOG_ERROR, "Failed to load level config");
1205 return;
1206 }
1207
1208 ParsedGameData gameData = {0};
1209 if (ParseGameData(&gameData, config))
1210 {
1211 for (int i = 0; i < 8; i++)
1212 {
1213 EnemyClassConfig *enemyClassConfig = &gameData.enemyClasses[i];
1214 if (enemyClassConfig->health > 0.0f)
1215 {
1216 enemyClassConfigs[i] = *enemyClassConfig;
1217 }
1218 }
1219
1220 for (int i = 0; i < 32; i++)
1221 {
1222 Level *level = &gameData.levels[i];
1223 if (level->initialGold > 0)
1224 {
1225 loadedLevels[i] = *level;
1226 }
1227 }
1228
1229 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
1230 {
1231 TowerTypeConfig *towerTypeConfig = &gameData.towerTypes[i];
1232 if (towerTypeConfig->maxHealth > 0)
1233 {
1234 TowerTypeSetData(i, towerTypeConfig);
1235 }
1236 }
1237 }
1238
1239 UnloadFileText(config);
1240 }
1241
1242 void InitGame()
1243 {
1244 TowerInit();
1245 EnemyInit();
1246 ProjectileInit();
1247 ParticleInit();
1248 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1249
1250 LoadConfig();
1251 currentLevel = loadedLevels[0].initialGold > 0 ? loadedLevels : currentLevel;
1252
1253 InitLevel(currentLevel);
1254 }
1255
1256 //# Immediate GUI functions
1257
1258 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1259 {
1260 const float healthBarHeight = 6.0f;
1261 const float healthBarOffset = 15.0f;
1262 const float inset = 2.0f;
1263 const float innerWidth = healthBarWidth - inset * 2;
1264 const float innerHeight = healthBarHeight - inset * 2;
1265
1266 Vector2 screenPos = GetWorldToScreen(position, camera);
1267 screenPos = Vector2Add(screenPos, screenOffset);
1268 float centerX = screenPos.x - healthBarWidth * 0.5f;
1269 float topY = screenPos.y - healthBarOffset;
1270 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1271 float healthWidth = innerWidth * healthRatio;
1272 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1273 }
1274
1275 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1276 {
1277 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1278
1279 DrawTextEx(gameFontNormal, text, (Vector2){
1280 x + (width - textSize.x) * alignX,
1281 y + (height - textSize.y) * alignY
1282 }, gameFontNormal.baseSize, 1, textColor);
1283 }
1284
1285 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1286 {
1287 Rectangle bounds = {x, y, width, height};
1288 int isPressed = 0;
1289 int isSelected = state && state->isSelected;
1290 int isDisabled = state && state->isDisabled;
1291 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1292 {
1293 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1294 {
1295 isPressed = 1;
1296 }
1297 guiState.isBlocked = 1;
1298 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1299 bounds, Vector2Zero(), 0, WHITE);
1300 }
1301 else
1302 {
1303 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1304 bounds, Vector2Zero(), 0, WHITE);
1305 }
1306 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1307 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1308 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1309 return isPressed;
1310 }
1311
1312 //# Main game loop
1313
1314 void GameUpdate()
1315 {
1316 UpdateLevel(currentLevel);
1317 }
1318
1319 #ifdef PLATFORM_WEB
1320 void InitWeb()
1321 {
1322 // create button that adds a textarea with the data/level.txt content
1323 // together with a button to load the data
1324 char *js = LoadFileText("data/html-edit.js");
1325 emscripten_run_script(js);
1326 UnloadFileText(js);
1327 }
1328 #else
1329 void InitWeb()
1330 {
1331
1332 }
1333 #endif
1334
1335 int main(void)
1336 {
1337 int screenWidth, screenHeight;
1338 GetPreferredSize(&screenWidth, &screenHeight);
1339 InitWindow(screenWidth, screenHeight, "Tower defense");
1340 float gamespeed = 1.0f;
1341 int frameRate = 30;
1342 SetTargetFPS(30);
1343
1344 LoadAssets();
1345 InitGame();
1346
1347 InitWeb();
1348
1349 float pause = 1.0f;
1350
1351 while (!WindowShouldClose())
1352 {
1353 if (IsPaused()) {
1354 // canvas is not visible in browser - do nothing
1355 continue;
1356 }
1357
1358 if (IsKeyPressed(KEY_F))
1359 {
1360 frameRate = (frameRate + 5) % 30;
1361 frameRate = frameRate < 10 ? 10 : frameRate;
1362 SetTargetFPS(frameRate);
1363 }
1364
1365 if (IsKeyPressed(KEY_T))
1366 {
1367 gamespeed += 0.1f;
1368 if (gamespeed > 1.05f) gamespeed = 0.1f;
1369 }
1370
1371 if (IsKeyPressed(KEY_P))
1372 {
1373 pause = pause > 0.5f ? 0.0f : 1.0f;
1374 }
1375
1376 float dt = GetFrameTime() * gamespeed * pause;
1377 // cap maximum delta time to 0.1 seconds to prevent large time steps
1378 if (dt > 0.1f) dt = 0.1f;
1379 gameTime.time += dt;
1380 gameTime.deltaTime = dt;
1381 gameTime.frameCount += 1;
1382
1383 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1384 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1385
1386 BeginDrawing();
1387 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1388
1389 GameUpdate();
1390 DrawLevel(currentLevel);
1391
1392 if (gamespeed != 1.0f)
1393 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1394 EndDrawing();
1395
1396 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1397 }
1398
1399 CloseWindow();
1400
1401 return 0;
1402 }
1 #include "td_main.h"
2 #include <raylib.h>
3 #include <stdio.h>
4 #include <string.h>
5
6 typedef struct ParserState
7 {
8 char *input;
9 int position;
10 char nextToken[256];
11 } ParserState;
12
13 int ParserStateGetLineNumber(ParserState *state)
14 {
15 int lineNumber = 1;
16 for (int i = 0; i < state->position; i++)
17 {
18 if (state->input[i] == '\n')
19 {
20 lineNumber++;
21 }
22 }
23 return lineNumber;
24 }
25
26 void ParserStateSkipWhiteSpaces(ParserState *state)
27 {
28 char *input = state->input;
29 int pos = state->position;
30 int skipped = 1;
31 while (skipped)
32 {
33 skipped = 0;
34 if (input[pos] == '-' && input[pos + 1] == '-')
35 {
36 skipped = 1;
37 // skip comments
38 while (input[pos] != 0 && input[pos] != '\n')
39 {
40 pos++;
41 }
42 }
43
44 // skip white spaces and ignore colons
45 while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
46 {
47 skipped = 1;
48 pos++;
49 }
50
51 // repeat until no more white spaces or comments
52 }
53 state->position = pos;
54 }
55
56 int ParserStateReadNextToken(ParserState *state)
57 {
58 ParserStateSkipWhiteSpaces(state);
59
60 int i = 0, pos = state->position;
61 char *input = state->input;
62
63 // read token
64 while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
65 {
66 state->nextToken[i] = input[pos];
67 pos++;
68 i++;
69 }
70 state->position = pos;
71
72 if (i == 0 || i == 256)
73 {
74 state->nextToken[0] = 0;
75 return 0;
76 }
77 // terminate the token
78 state->nextToken[i] = 0;
79 return 1;
80 }
81
82 int ParserStateReadNextInt(ParserState *state, int *value)
83 {
84 if (!ParserStateReadNextToken(state))
85 {
86 return 0;
87 }
88 // check if the token is a valid integer
89 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
90 for (int i = isSigned; state->nextToken[i] != 0; i++)
91 {
92 if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
93 {
94 return 0;
95 }
96 }
97 *value = TextToInteger(state->nextToken);
98 return 1;
99 }
100
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103 if (!ParserStateReadNextToken(state))
104 {
105 return 0;
106 }
107 // check if the token is a valid float number
108 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109 int hasDot = 0;
110 for (int i = isSigned; state->nextToken[i] != 0; i++)
111 {
112 if (state->nextToken[i] == '.')
113 {
114 if (hasDot)
115 {
116 return 0;
117 }
118 hasDot = 1;
119 }
120 else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121 {
122 return 0;
123 }
124 }
125
126 *value = TextToFloat(state->nextToken);
127 return 1;
128 }
129
130 typedef enum TryReadResult
131 {
132 TryReadResult_NoMatch,
133 TryReadResult_Error,
134 TryReadResult_Success
135 } TryReadResult;
136
137 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
138 {
139 gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
140 return TryReadResult_Error;
141 }
142
143 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
144 {
145 if (!TextIsEqual(state->nextToken, key))
146 {
147 return TryReadResult_NoMatch;
148 }
149
150 if (!ParserStateReadNextInt(state, value))
151 {
152 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
153 }
154
155 // range test, if minRange == maxRange, we don't check the range
156 if (minRange != maxRange && (*value < minRange || *value > maxRange))
157 {
158 return ParseGameDataError(gameData, state, TextFormat(
159 "Invalid value range for %s, range is [%d, %d], value is %d",
160 key, minRange, maxRange, *value));
161 }
162
163 return TryReadResult_Success;
164 }
165
166 TryReadResult ParseGameDataTryReadKeyUInt8(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *value, uint8_t minRange, uint8_t maxRange)
167 {
168 int intValue = *value;
169 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
170 *value = (uint8_t) intValue;
171 return result;
172 }
173
174 TryReadResult ParseGameDataTryReadKeyInt16(ParsedGameData *gameData, ParserState *state, const char *key, int16_t *value, int16_t minRange, int16_t maxRange)
175 {
176 int intValue = *value;
177 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
178 *value = (int16_t) intValue;
179 return result;
180 }
181
182 TryReadResult ParseGameDataTryReadKeyUInt16(ParsedGameData *gameData, ParserState *state, const char *key, uint16_t *value, uint16_t minRange, uint16_t maxRange)
183 {
184 int intValue = *value;
185 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
186 *value = (uint16_t) intValue;
187 return result;
188 }
189
190 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key,
191 Vector2 *vector, Vector2 minRange, Vector2 maxRange)
192 {
193 if (!TextIsEqual(state->nextToken, key))
194 {
195 return TryReadResult_NoMatch;
196 }
197
198 ParserState start = *state;
199 int x = 0, y = 0;
200 int minXRange = (int)minRange.x, maxXRange = (int)maxRange.x;
201 int minYRange = (int)minRange.y, maxYRange = (int)maxRange.y;
202
203 if (!ParserStateReadNextInt(state, &x))
204 {
205 // use start position to report the error for this KEY
206 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
207 }
208
209 // range test, if minRange == maxRange, we don't check the range
210 if (minXRange != maxXRange && (x < minXRange || x > maxXRange))
211 {
212 // use current position to report the error for x value
213 return ParseGameDataError(gameData, state, TextFormat(
214 "Invalid value x range for %s, range is [%d, %d], value is %d",
215 key, minXRange, maxXRange, x));
216 }
217
218 if (!ParserStateReadNextInt(state, &y))
219 {
220 // use start position to report the error for this KEY
221 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key));
222 }
223
224 if (minYRange != maxYRange && (y < minYRange || y > maxYRange))
225 {
226 // use current position to report the error for y value
227 return ParseGameDataError(gameData, state, TextFormat(
228 "Invalid value y range for %s, range is [%d, %d], value is %d",
229 key, minYRange, maxYRange, y));
230 }
231
232 vector->x = (float)x;
233 vector->y = (float)y;
234
235 return TryReadResult_Success;
236 }
237
238 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
239 {
240 if (!TextIsEqual(state->nextToken, key))
241 {
242 return TryReadResult_NoMatch;
243 }
244
245 if (!ParserStateReadNextFloat(state, value))
246 {
247 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
248 }
249
250 // range test, if minRange == maxRange, we don't check the range
251 if (minRange != maxRange && (*value < minRange || *value > maxRange))
252 {
253 return ParseGameDataError(gameData, state, TextFormat(
254 "Invalid value range for %s, range is [%f, %f], value is %f",
255 key, minRange, maxRange, *value));
256 }
257
258 return TryReadResult_Success;
259 }
260
261 // The enumNames is a null-terminated array of strings that represent the enum values
262 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues)
263 {
264 if (!TextIsEqual(state->nextToken, key))
265 {
266 return TryReadResult_NoMatch;
267 }
268
269 if (!ParserStateReadNextToken(state))
270 {
271 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key));
272 }
273
274 for (int i = 0; enumNames[i] != 0; i++)
275 {
276 if (TextIsEqual(state->nextToken, enumNames[i]))
277 {
278 *value = enumValues[i];
279 return TryReadResult_Success;
280 }
281 }
282
283 return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key));
284 }
285
286 TryReadResult ParseGameDataTryReadKeyEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId, uint8_t minRange, uint8_t maxRange)
287 {
288 int enemyClassId = *enemyTypeId;
289 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId,
290 (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0},
291 (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS});
292 if (minRange != maxRange)
293 {
294 enemyClassId = enemyClassId < minRange ? minRange : enemyClassId;
295 enemyClassId = enemyClassId > maxRange ? maxRange : enemyClassId;
296 }
297 *enemyTypeId = (uint8_t) enemyClassId;
298 return result;
299 }
300
301 TryReadResult ParseGameDataTryReadKeyTowerTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
302 {
303 int towerType = *towerTypeId;
304 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType,
305 (const char *[]){"TOWER_TYPE_BASE", "TOWER_TYPE_ARCHER", "TOWER_TYPE_BALLISTA", "TOWER_TYPE_CATAPULT", "TOWER_TYPE_WALL", 0},
306 (int[]){TOWER_TYPE_BASE, TOWER_TYPE_ARCHER, TOWER_TYPE_BALLISTA, TOWER_TYPE_CATAPULT, TOWER_TYPE_WALL});
307 if (minRange != maxRange)
308 {
309 towerType = towerType < minRange ? minRange : towerType;
310 towerType = towerType > maxRange ? maxRange : towerType;
311 }
312 *towerTypeId = (uint8_t) towerType;
313 return result;
314 }
315
316 TryReadResult ParseGameDataTryReadKeyProjectileTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
317 {
318 int towerType = *towerTypeId;
319 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType,
320 (const char *[]){"PROJECTILE_TYPE_ARROW", "PROJECTILE_TYPE_BALLISTA", "PROJECTILE_TYPE_CATAPULT", 0},
321 (int[]){PROJECTILE_TYPE_ARROW, PROJECTILE_TYPE_BALLISTA, PROJECTILE_TYPE_CATAPULT});
322 if (minRange != maxRange)
323 {
324 towerType = towerType < minRange ? minRange : towerType;
325 towerType = towerType > maxRange ? maxRange : towerType;
326 }
327 *towerTypeId = (uint8_t) towerType;
328 return result;
329 }
330
331
332 //----------------------------------------------------------------
333 //# Defines for compact struct field parsing
334 // A FIELDS(GENERATEr) is to be defined that will be called for each field of the struct
335 // See implementations below for how this is used
336 #define GENERATE_READFIELD_SWITCH(owner, name, type, min, max)\
337 switch (ParseGameDataTryReadKey##type(gameData, state, #name, &owner->name, min, max))\
338 {\
339 case TryReadResult_NoMatch: break;\
340 case TryReadResult_Success:\
341 if (name##Initialized) {\
342 return ParseGameDataError(gameData, state, #name " already initialized");\
343 }\
344 name##Initialized = 1;\
345 continue;\
346 case TryReadResult_Error: return TryReadResult_Error;\
347 }
348 #define GENERATE_READFIELD_SWITCH_OPTIONAL(owner, name, type, def, min, max)\
349 GENERATE_READFIELD_SWITCH(owner, name, type, min, max)
350 #define GENERATE_FIELD_INIT_DECLARATIONS(owner, name, type, min, max) int name##Initialized = 0;
351 #define GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL(owner, name, type, def, min, max) int name##Initialized = 0; owner->name = def;
352 #define GENERATE_FIELD_INIT_CHECK(owner, name, type, min, max) \
353 if (!name##Initialized) { \
354 return ParseGameDataError(gameData, state, #name " not initialized"); \
355 }
356 #define GENERATE_FIELD_INIT_CHECK_OPTIONAL(owner, name, type, def, min, max)
357
358 #define GENERATE_FIELD_PARSING \
359 FIELDS(GENERATE_FIELD_INIT_DECLARATIONS, GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL)\
360 while (1)\
361 {\
362 ParserState prevState = *state;\
363 \
364 if (!ParserStateReadNextToken(state))\
365 {\
366 /* end of file */\
367 break;\
368 }\
369 FIELDS(GENERATE_READFIELD_SWITCH, GENERATE_READFIELD_SWITCH_OPTIONAL)\
370 /* no match, return to previous state and break */\
371 *state = prevState;\
372 break;\
373 } \
374 FIELDS(GENERATE_FIELD_INIT_CHECK, GENERATE_FIELD_INIT_CHECK_OPTIONAL)\
375
376 // END OF DEFINES
377
378 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state)
379 {
380 if (!TextIsEqual(state->nextToken, "Wave"))
381 {
382 return TryReadResult_NoMatch;
383 }
384
385 Level *level = &gameData->levels[gameData->lastLevelIndex];
386 EnemyWave *wave = 0;
387 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
388 {
389 if (level->waves[i].count == 0)
390 {
391 wave = &level->waves[i];
392 break;
393 }
394 }
395
396 if (wave == 0)
397 {
398 return ParseGameDataError(gameData, state,
399 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT));
400 }
401
402 #define FIELDS(MANDATORY, OPTIONAL) \
403 MANDATORY(wave, wave, UInt8, 0, ENEMY_MAX_WAVE_COUNT - 1) \
404 MANDATORY(wave, count, UInt16, 1, 1000) \
405 MANDATORY(wave, delay, Float, 0.0f, 1000.0f) \
406 MANDATORY(wave, interval, Float, 0.0f, 1000.0f) \
407 MANDATORY(wave, spawnPosition, IntVec2, ((Vector2){-10.0f, -10.0f}), ((Vector2){10.0f, 10.0f})) \
408 MANDATORY(wave, enemyType, EnemyTypeId, 0, 0)
409
410 GENERATE_FIELD_PARSING
411 #undef FIELDS
412
413 return TryReadResult_Success;
414 }
415
416 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state)
417 {
418 uint8_t enemyClassId;
419
420 switch (ParseGameDataTryReadKeyEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId, 0, 7))
421 {
422 case TryReadResult_Success: break;
423 case TryReadResult_NoMatch: return TryReadResult_NoMatch;
424 case TryReadResult_Error: return TryReadResult_Error;
425 }
426
427 EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId];
428
429 #define FIELDS(MANDATORY, OPTIONAL) \
430 MANDATORY(enemyClass, speed, Float, 0.1f, 1000.0f) \
431 MANDATORY(enemyClass, health, Float, 1, 1000000) \
432 MANDATORY(enemyClass, radius, Float, 0.0f, 10.0f) \
433 MANDATORY(enemyClass, requiredContactTime, Float, 0.0f, 1000.0f) \
434 MANDATORY(enemyClass, maxAcceleration, Float, 0.0f, 1000.0f) \
435 MANDATORY(enemyClass, explosionDamage, Float, 0.0f, 1000.0f) \
436 MANDATORY(enemyClass, explosionRange, Float, 0.0f, 1000.0f) \
437 MANDATORY(enemyClass, explosionPushbackPower, Float, 0.0f, 1000.0f) \
438 MANDATORY(enemyClass, goldValue, Int, 1, 1000000)
439
440 GENERATE_FIELD_PARSING
441 #undef FIELDS
442
443 return TryReadResult_Success;
444 }
445
446 TryReadResult ParseGameDataTryReadTowerTypeConfigSection(ParsedGameData *gameData, ParserState *state)
447 {
448 uint8_t towerTypeId;
449
450 switch (ParseGameDataTryReadKeyTowerTypeId(gameData, state, "TowerTypeConfig", &towerTypeId, 0, TOWER_TYPE_COUNT - 1))
451 {
452 case TryReadResult_Success: break;
453 case TryReadResult_NoMatch: return TryReadResult_NoMatch;
454 case TryReadResult_Error: return TryReadResult_Error;
455 }
456
457 TowerTypeConfig *towerType = &gameData->towerTypes[towerTypeId];
458 HitEffectConfig *hitEffect = &towerType->hitEffect;
459
460 #define FIELDS(MANDATORY, OPTIONAL) \
461 MANDATORY(towerType, maxHealth, UInt16, 0, 0) \
462 OPTIONAL(towerType, cooldown, Float, 0, 0.0f, 1000.0f) \
463 OPTIONAL(towerType, maxUpgradeCooldown, Float, 0, 0.0f, 1000.0f) \
464 OPTIONAL(towerType, range, Float, 0, 0.0f, 50.0f) \
465 OPTIONAL(towerType, maxUpgradeRange, Float, 0, 0.0f, 50.0f) \
466 OPTIONAL(towerType, projectileSpeed, Float, 0, 0.0f, 100.0f) \
467 OPTIONAL(towerType, cost, UInt8, 0, 0, 255) \
468 OPTIONAL(towerType, projectileType, ProjectileTypeId, 0, 0, 32)\
469 OPTIONAL(hitEffect, damage, Float, 0, 0, 100000.0f) \
470 OPTIONAL(hitEffect, maxUpgradeDamage, Float, 0, 0, 100000.0f) \
471 OPTIONAL(hitEffect, areaDamageRadius, Float, 0, 0, 100000.0f) \
472 OPTIONAL(hitEffect, pushbackPowerDistance, Float, 0, 0, 100000.0f)
473
474 GENERATE_FIELD_PARSING
475 #undef FIELDS
476
477 return TryReadResult_Success;
478 }
479
480 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state)
481 {
482 int levelId;
483 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31);
484 if (result != TryReadResult_Success)
485 {
486 return result;
487 }
488
489 gameData->lastLevelIndex = levelId;
490 Level *level = &gameData->levels[levelId];
491
492 // since we require the initialGold to be initialized with at least 1, we can use it as a flag
493 // to detect if the level was already initialized
494 if (level->initialGold != 0)
495 {
496 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId));
497 }
498
499 int initialGoldInitialized = 0;
500
501 while (1)
502 {
503 // try to read the next token and if we don't know how to GENERATE it,
504 // we rewind and return
505 ParserState prevState = *state;
506
507 if (!ParserStateReadNextToken(state))
508 {
509 // end of file
510 break;
511 }
512
513 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000))
514 {
515 case TryReadResult_Success:
516 if (initialGoldInitialized)
517 {
518 return ParseGameDataError(gameData, state, "initialGold already initialized");
519 }
520 initialGoldInitialized = 1;
521 continue;
522 case TryReadResult_Error: return TryReadResult_Error;
523 case TryReadResult_NoMatch: break;
524 }
525
526 switch (ParseGameDataTryReadWaveSection(gameData, state))
527 {
528 case TryReadResult_Success: continue;
529 case TryReadResult_NoMatch: break;
530 case TryReadResult_Error: return TryReadResult_Error;
531 }
532
533 // no match, return to previous state and break
534 *state = prevState;
535 break;
536 }
537
538 if (!initialGoldInitialized)
539 {
540 return ParseGameDataError(gameData, state, "initialGold not initialized");
541 }
542
543 return TryReadResult_Success;
544 }
545
546 int ParseGameData(ParsedGameData *gameData, const char *input)
547 {
548 ParserState state = (ParserState){(char *)input, 0, {0}};
549 *gameData = (ParsedGameData){0};
550 gameData->lastLevelIndex = -1;
551
552 while (ParserStateReadNextToken(&state))
553 {
554 switch (ParseGameDataTryReadLevelSection(gameData, &state))
555 {
556 case TryReadResult_Success: continue;
557 case TryReadResult_Error: return 0;
558 case TryReadResult_NoMatch: break;
559 }
560
561 switch (ParseGameDataTryReadEnemyClassSection(gameData, &state))
562 {
563 case TryReadResult_Success: continue;
564 case TryReadResult_Error: return 0;
565 case TryReadResult_NoMatch: break;
566 }
567
568 switch (ParseGameDataTryReadTowerTypeConfigSection(gameData, &state))
569 {
570 case TryReadResult_Success: continue;
571 case TryReadResult_Error: return 0;
572 case TryReadResult_NoMatch: break;
573 }
574
575 // any other token is considered an error
576 ParseGameDataError(gameData, &state, TextFormat("Unexpected token: %s", state.nextToken));
577 return 0;
578 }
579
580 return 1;
581 }
582
583 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; }
584
585 int RunParseTests()
586 {
587 int passedCount = 0, failedCount = 0;
588 ParsedGameData gameData;
589 const char *input;
590
591 input ="Level 7\n initialGold 100\nLevel 2 initialGold 200";
592 gameData = (ParsedGameData) {0};
593 EXPECT(ParseGameData(&gameData, input) == 1, "Failed to parse level section");
594 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2");
595 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100");
596 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200");
597
598 input ="Level 392\n";
599 gameData = (ParsedGameData) {0};
600 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
601 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error");
602 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1");
603
604 input ="Level 3\n initialGold -34";
605 gameData = (ParsedGameData) {0};
606 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
607 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error");
608 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1");
609
610 input ="Level 3\n initialGold 2\n initialGold 3";
611 gameData = (ParsedGameData) {0};
612 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
613 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1");
614
615 input ="Level 3";
616 gameData = (ParsedGameData) {0};
617 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
618
619 input ="Level 7\n initialGold 100\nLevel 7 initialGold 200";
620 gameData = (ParsedGameData) {0};
621 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
622 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1");
623
624 input =
625 "Level 7\n initialGold 100\n"
626 "Wave\n"
627 "count 1 wave 2\n"
628 "interval 0.5\n"
629 "delay 1.0\n"
630 "spawnPosition -3 4\n"
631 "enemyType: ENEMY_TYPE_SHIELD"
632 ;
633 gameData = (ParsedGameData) {0};
634 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing level/wave section");
635 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1");
636 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2");
637 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5");
638 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0");
639 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3");
640 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4");
641 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD");
642
643 // for every entry in the wave section, we want to verify that if that value is
644 // missing, the parser will produce an error. We can do that by commenting out each
645 // line individually in a loop - just replacing the two leading spaces with two dashes
646 const char *testString =
647 "Level 7 initialGold 100\n"
648 "Wave\n"
649 " count 1\n"
650 " wave 2\n"
651 " interval 0.5\n"
652 " delay 1.0\n"
653 " spawnPosition 3 -4\n"
654 " enemyType: ENEMY_TYPE_SHIELD";
655 for (int i = 0; testString[i]; i++)
656 {
657 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ')
658 {
659 char copy[1024];
660 strcpy(copy, testString);
661 // commentify!
662 copy[i + 1] = '-';
663 copy[i + 2] = '-';
664 gameData = (ParsedGameData) {0};
665 EXPECT(ParseGameData(&gameData, copy) == 0, "Expected to fail parsing level/wave section");
666 }
667 }
668
669 // test wave section missing data / incorrect data
670
671 input =
672 "Level 7\n initialGold 100\n"
673 "Wave\n"
674 "count 1 wave 2\n"
675 "interval 0.5\n"
676 "delay 1.0\n"
677 "spawnPosition -3\n" // missing y
678 "enemyType: ENEMY_TYPE_SHIELD"
679 ;
680 gameData = (ParsedGameData) {0};
681 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level/wave section");
682 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7");
683
684 input =
685 "Level 7\n initialGold 100\n"
686 "Wave\n"
687 "count 1.0 wave 2\n"
688 "interval 0.5\n"
689 "delay 1.0\n"
690 "spawnPosition -3\n" // missing y
691 "enemyType: ENEMY_TYPE_SHIELD"
692 ;
693 gameData = (ParsedGameData) {0};
694 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level/wave section");
695 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3");
696
697 // enemy class config parsing tests
698 input =
699 "EnemyClass ENEMY_TYPE_MINION\n"
700 " health: 10.0\n"
701 " speed: 0.6\n"
702 " radius: 0.25\n"
703 " maxAcceleration: 1.0\n"
704 " explosionDamage: 1.0\n"
705 " requiredContactTime: 0.5\n"
706 " explosionRange: 1.0\n"
707 " explosionPushbackPower: 0.25\n"
708 " goldValue: 1\n"
709 ;
710 gameData = (ParsedGameData) {0};
711 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing enemy class section");
712 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0");
713 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6");
714 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25");
715 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0");
716 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0");
717 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5");
718 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0");
719 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25");
720 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1");
721
722 testString =
723 "EnemyClass ENEMY_TYPE_MINION\n"
724 " health: 10.0\n"
725 " speed: 0.6\n"
726 " radius: 0.25\n"
727 " maxAcceleration: 1.0\n"
728 " explosionDamage: 1.0\n"
729 " requiredContactTime: 0.5\n"
730 " explosionRange: 1.0\n"
731 " explosionPushbackPower: 0.25\n"
732 " goldValue: 1\n";
733 for (int i = 0; testString[i]; i++)
734 {
735 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ')
736 {
737 char copy[1024];
738 strcpy(copy, testString);
739 // commentify!
740 copy[i + 1] = '-';
741 copy[i + 2] = '-';
742 gameData = (ParsedGameData) {0};
743 EXPECT(ParseGameData(&gameData, copy) == 0, "Expected to fail parsing EnemyClass section");
744 }
745 }
746
747 input =
748 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
749 " cooldown: 0.5\n"
750 " maxUpgradeCooldown: 0.25\n"
751 " range: 3\n"
752 " maxUpgradeRange: 5\n"
753 " projectileSpeed: 4.0\n"
754 " cost: 5\n"
755 " maxHealth: 10\n"
756 " projectileType: PROJECTILE_TYPE_ARROW\n"
757 " damage: 0.5\n"
758 " maxUpgradeDamage: 1.5\n"
759 " areaDamageRadius: 0\n"
760 " pushbackPowerDistance: 0\n"
761 ;
762 gameData = (ParsedGameData) {0};
763 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing tower type section");
764 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5");
765 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeCooldown == 0.25f, "Expected maxUpgradeCooldown to be 0.25");
766 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].range == 3.0f, "Expected range to be 3.0");
767 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeRange == 5.0f, "Expected maxUpgradeRange to be 5.0");
768 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileSpeed == 4.0f, "Expected projectileSpeed to be 4.0");
769 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 5, "Expected cost to be 5");
770 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
771 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileType == PROJECTILE_TYPE_ARROW, "Expected projectileType to be PROJECTILE_TYPE_ARROW");
772 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.damage == 0.5f, "Expected damage to be 0.5");
773 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.maxUpgradeDamage == 1.5f, "Expected maxUpgradeDamage to be 1.5");
774 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.areaDamageRadius == 0.0f, "Expected areaDamageRadius to be 0.0");
775 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.pushbackPowerDistance == 0.0f, "Expected pushbackPowerDistance to be 0.0");
776
777 input =
778 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
779 " maxHealth: 10\n"
780 " cooldown: 0.5\n"
781 ;
782 gameData = (ParsedGameData) {0};
783 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing tower type section");
784 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5");
785 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
786 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0");
787
788
789 input =
790 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
791 " cooldown: 0.5\n"
792 ;
793 gameData = (ParsedGameData) {0};
794 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing tower type section");
795 EXPECT(TextFindIndex(gameData.parseError, "maxHealth not initialized") >= 0, "Expected to find maxHealth not initialized");
796
797 input =
798 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
799 " maxHealth: 10\n"
800 " foobar: 0.5\n"
801 ;
802 gameData = (ParsedGameData) {0};
803 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing tower type section");
804 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
805 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0");
806
807 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount);
808
809 return failedCount;
810 }
1 var editButton = document.createElement('button');
2 editButton.innerHTML = 'Edit level data';
3 Module.canvas.insertAdjacentElement('afterend', editButton);
4
5 editButton.onclick = function () {
6 editButton.style.display = 'none';
7 let container = document.createElement('div');
8 container.innerHTML = `
9 <div style='display: flex; align-items: flex-start; height: 400px; overflow: hidden;'>
10 <div class='line-numbers' style='padding: 10px 0px; overflow-y: auto; height: 100%;
11 scrollbar-width: none; -ms-overflow-style: none; white-space: pre;
12 background:rgb(44, 45, 48); color:#ddd; text-align: right; user-select: none; font-family: monospace;'></div>
13 <textarea class='textarea' rows='20' cols='80' style='
14 width: 100%; height: 100%; padding: 10px; margin:0; border: none; outline: none; resize: none;
15 overflow-y: auto; font-family: monospace;'></textarea>
16 </div>
17 <button>Load data</button>
18 `
19 Module.canvas.insertAdjacentElement('afterend', container);
20
21
22 const codeArea = container.querySelector('.textarea');
23 const lineNumbers = container.querySelector('.line-numbers');
24 const loadButton = container.querySelector('button');
25 var errorLines = { };
26 function updateLineNumbers() {
27 const lines = codeArea.value.split('\n').length;
28 lineNumbers.innerHTML = Array.from({ length: lines },
29 (_, i) => {
30 let error = errorLines[i + 1];
31 if (error) {
32 return `<div style="background:#833;" title="${error}"> ${i + 1} </div>`;
33 }
34 return `<div style=""> ${i + 1} </div>`;
35 }
36 ).join('');
37 }
38 function syncScroll() {
39 lineNumbers.scrollTop = codeArea.scrollTop;
40 }
41
42 codeArea.addEventListener('input', updateLineNumbers);
43 codeArea.addEventListener('scroll', syncScroll);
44 lineNumbers.addEventListener('scroll', syncScroll);
45
46 loadButton.onclick = function () {
47 var levelData = codeArea.value;
48 FS.writeFile('data/level.txt', levelData);
49 Module._RequestReload();
50 }
51
52 codeArea.value = FS.readFile('data/level.txt', { encoding: 'utf8' });
53
54 updateLineNumbers();
55
56 // Function to highlight lines with errors
57 function highlightErrorLines(lines) {
58 const codeLines = codeArea.value.split('\n');
59 const highlightedCode = codeLines.map((line, index) => {
60 return lines.includes(index + 1) ? `<span class="highlight">${line}</span>` : line;
61 }).join('\n');
62 codeArea.innerHTML = highlightedCode;
63 }
64
65 // Example usage: Highlight lines 2 and 4
66 // highlightErrorLines([2, 4]);
67
68 }
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 typedef struct ParsedGameData
286 {
287 const char *parseError;
288 Level levels[32];
289 int lastLevelIndex;
290 TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
291 EnemyClassConfig enemyClasses[8];
292 } ParsedGameData;
293
294 //# Function declarations
295 int ParseGameData(ParsedGameData *gameData, const char *input);
296 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
297 int EnemyAddDamageRange(Vector2 position, float range, float damage);
298 int EnemyAddDamage(Enemy *enemy, float damage);
299
300 //# Enemy functions
301 void EnemyInit();
302 void EnemyDraw();
303 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
304 void EnemyUpdate();
305 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
306 float EnemyGetMaxHealth(Enemy *enemy);
307 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
308 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
309 EnemyId EnemyGetId(Enemy *enemy);
310 Enemy *EnemyTryResolve(EnemyId enemyId);
311 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
312 int EnemyAddDamage(Enemy *enemy, float damage);
313 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
314 int EnemyCount();
315 void EnemyDrawHealthbars(Camera3D camera);
316
317 //# Tower functions
318 const char *TowerTypeGetName(uint8_t towerType);
319 int TowerTypeGetCosts(uint8_t towerType);
320 void TowerTypeSetData(uint8_t towerType, TowerTypeConfig *data);
321 void TowerInit();
322 void TowerUpdate();
323 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget);
324 void TowerDrawAll();
325 void TowerDrawAllHealthBars(Camera3D camera);
326 void TowerDrawModel(Tower *tower);
327 void TowerDrawRange(Tower *tower, float alpha);
328 Tower *TowerGetByIndex(int index);
329 Tower *TowerGetByType(uint8_t towerType);
330 Tower *TowerGetAt(int16_t x, int16_t y);
331 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
332 float TowerGetMaxHealth(Tower *tower);
333 float TowerGetRange(Tower *tower);
334
335 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
336
337 //# Particles
338 void ParticleInit();
339 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
340 void ParticleUpdate();
341 void ParticleDraw();
342
343 //# Projectiles
344 void ProjectileInit();
345 void ProjectileDraw();
346 void ProjectileUpdate();
347 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
348
349 //# Pathfinding map
350 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
351 float PathFindingGetDistance(int mapX, int mapY);
352 Vector2 PathFindingGetGradient(Vector3 world);
353 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
354 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
355 void PathFindingMapDraw();
356
357 //# UI
358 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
359
360 //# Level
361 void DrawLevelGround(Level *level);
362 void DrawEnemyPath(Level *level, Color arrowColor);
363
364 //# variables
365 extern Level *currentLevel;
366 extern Enemy enemies[ENEMY_MAX_COUNT];
367 extern int enemyCount;
368 extern EnemyClassConfig enemyClassConfigs[];
369
370 extern GUIState guiState;
371 extern GameTime gameTime;
372 extern Tower towers[TOWER_MAX_COUNT];
373 extern int towerCount;
374
375 extern Texture2D palette, spriteSheet;
376
377 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .name = "Castle",
7 .maxHealth = 10,
8 },
9 [TOWER_TYPE_ARCHER] = {
10 .name = "Archer",
11 .cooldown = 0.5f,
12 .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 void TowerTypeSetData(uint8_t towerType, TowerTypeConfig *data)
208 {
209 towerTypeConfigs[towerType] = *data;
210 }
211
212 Tower *TowerGetAt(int16_t x, int16_t y)
213 {
214 for (int i = 0; i < towerCount; i++)
215 {
216 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
217 {
218 return &towers[i];
219 }
220 }
221 return 0;
222 }
223
224 Tower *TowerGetByIndex(int index)
225 {
226 if (index < 0 || index >= towerCount)
227 {
228 return 0;
229 }
230 return &towers[index];
231 }
232
233 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
234 {
235 if (towerCount >= TOWER_MAX_COUNT)
236 {
237 return 0;
238 }
239
240 Tower *tower = TowerGetAt(x, y);
241 if (tower)
242 {
243 return 0;
244 }
245
246 tower = &towers[towerCount++];
247 *tower = (Tower){
248 .x = x,
249 .y = y,
250 .towerType = towerType,
251 .cooldown = 0.0f,
252 .damage = 0.0f,
253 };
254 return tower;
255 }
256
257 Tower *TowerGetByType(uint8_t towerType)
258 {
259 for (int i = 0; i < towerCount; i++)
260 {
261 if (towers[i].towerType == towerType)
262 {
263 return &towers[i];
264 }
265 }
266 return 0;
267 }
268
269 const char *TowerTypeGetName(uint8_t towerType)
270 {
271 return towerTypeConfigs[towerType].name;
272 }
273
274 int TowerTypeGetCosts(uint8_t towerType)
275 {
276 return towerTypeConfigs[towerType].cost;
277 }
278
279 float TowerGetMaxHealth(Tower *tower)
280 {
281 return towerTypeConfigs[tower->towerType].maxHealth;
282 }
283
284 float TowerGetRange(Tower *tower)
285 {
286 float range = towerTypeConfigs[tower->towerType].range;
287 float maxUpgradeRange = towerTypeConfigs[tower->towerType].maxUpgradeRange;
288 if (tower->upgradeState.range > 0)
289 {
290 range = Lerp(range, maxUpgradeRange, tower->upgradeState.range / (float)TOWER_MAX_STAGE);
291 }
292 return range;
293 }
294
295 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget)
296 {
297 // animate fade in and fade out of range drawing using framerate independent lerp
298 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime);
299 for (int i = 0; i < TOWER_MAX_COUNT; i++)
300 {
301 Tower *fadingTower = TowerGetByIndex(i);
302 if (!fadingTower)
303 {
304 break;
305 }
306 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget;
307 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp);
308 }
309 }
310
311 void TowerDrawRange(Tower *tower, float alpha)
312 {
313 Color ringColor = (Color){255, 200, 100, 255};
314 const int rings = 4;
315 const float radiusOffset = 0.5f;
316 const float animationSpeed = 2.0f;
317 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
318 float radius = TowerGetRange(tower);
319 // base circle
320 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, radius, (Vector3){1, 0, 0}, 90,
321 Fade(ringColor, alpha));
322
323 for (int i = 1; i < rings; i++)
324 {
325 float t = ((float)i + animation) / (float)rings;
326 float r = Lerp(radius, radius - radiusOffset, t * t);
327 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
328 if (i == 1)
329 {
330 // fade out the outermost ring
331 a = animation;
332 }
333 a *= alpha;
334
335 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, r, (Vector3){1, 0, 0}, 90,
336 Fade(ringColor, a));
337 }
338 }
339
340 void TowerDrawModel(Tower *tower)
341 {
342 if (tower->towerType == TOWER_TYPE_NONE)
343 {
344 return;
345 }
346
347 if (tower->drawRangeAlpha > 2.0f/256.0f)
348 {
349 TowerDrawRange(tower, tower->drawRangeAlpha);
350 }
351
352 switch (tower->towerType)
353 {
354 case TOWER_TYPE_ARCHER:
355 {
356 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower->x, 0.0f, tower->y}, currentLevel->camera);
357 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower->lastTargetPosition.x, 0.0f, tower->lastTargetPosition.y}, currentLevel->camera);
358 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
359 DrawSpriteUnit(archerUnit, (Vector3){tower->x, 1.0f, tower->y}, 0, screenPosTarget.x > screenPosTower.x,
360 tower->cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
361 }
362 break;
363 case TOWER_TYPE_BALLISTA:
364 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, BROWN);
365 break;
366 case TOWER_TYPE_CATAPULT:
367 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
368 break;
369 default:
370 if (towerModels[tower->towerType].materials)
371 {
372 DrawModel(towerModels[tower->towerType], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
373 } else {
374 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
375 }
376 break;
377 }
378 }
379
380 void TowerDrawAll()
381 {
382 for (int i = 0; i < towerCount; i++)
383 {
384 TowerDrawModel(&towers[i]);
385 }
386 }
387
388 void TowerUpdate()
389 {
390 for (int i = 0; i < towerCount; i++)
391 {
392 Tower *tower = &towers[i];
393 switch (tower->towerType)
394 {
395 case TOWER_TYPE_CATAPULT:
396 case TOWER_TYPE_BALLISTA:
397 case TOWER_TYPE_ARCHER:
398 TowerGunUpdate(tower);
399 break;
400 }
401 }
402 }
403
404 void TowerDrawAllHealthBars(Camera3D camera)
405 {
406 for (int i = 0; i < towerCount; i++)
407 {
408 Tower *tower = &towers[i];
409 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
410 {
411 continue;
412 }
413
414 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
415 float maxHealth = TowerGetMaxHealth(tower);
416 float health = maxHealth - tower->damage;
417 float healthRatio = health / maxHealth;
418
419 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
420 }
421 }
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 "Load level" button is now created in the web version. When pressed, it adds a text area with the content of the configuration file. Editing the text and pressing the "Load level" button again will reload the game with the new configuration.
So now we have a way to modify the game configuration in the browser version!
There were just a few... workarounds necessary to make this work:
The GLFW emscripten implementation wants to prevent site navigation events getting triggered by pressing TAB or BACKSPACE. This is was pretty nasty to find a workaround for since this is deeply integrated into the GLFW emscripten implementation. I ended up overloading the window.addEventListener function to wrap functions that for keydown events and replacing the event.preventDefault function with a function that does nothing. This is now a script that is included in the HTML file of this page, so it is not very portable... but at least it works for this setup.
Now that we have a simple text editor in the browser, we can make a few improvements. Highlighting the line that contains an error is for example something pretty important to ease editing. There are quite a few JavaScript/HTML powered editors that could make this editor looks and feel like a full editor, but I want to keep things as minimal as possible.
So lets add some error logging and line highlighting.
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 #ifdef PLATFORM_WEB
8 #include <emscripten/emscripten.h>
9 #else
10 #define EMSCRIPTEN_KEEPALIVE
11 #endif
12
13 //# Variables
14 Font gameFontNormal = {0};
15 GUIState guiState = {0};
16 GameTime gameTime = {
17 .fixedDeltaTime = 1.0f / 60.0f,
18 };
19
20 Model floorTileAModel = {0};
21 Model floorTileBModel = {0};
22 Model treeModel[2] = {0};
23 Model firTreeModel[2] = {0};
24 Model rockModels[5] = {0};
25 Model grassPatchModel[1] = {0};
26
27 Model pathArrowModel = {0};
28 Model greenArrowModel = {0};
29
30 Texture2D palette, spriteSheet;
31
32 NPatchInfo uiPanelPatch = {
33 .layout = NPATCH_NINE_PATCH,
34 .source = {145, 1, 46, 46},
35 .top = 18, .bottom = 18,
36 .left = 16, .right = 16
37 };
38 NPatchInfo uiButtonNormal = {
39 .layout = NPATCH_NINE_PATCH,
40 .source = {193, 1, 32, 20},
41 .top = 7, .bottom = 7,
42 .left = 10, .right = 10
43 };
44 NPatchInfo uiButtonDisabled = {
45 .layout = NPATCH_NINE_PATCH,
46 .source = {193, 22, 32, 20},
47 .top = 7, .bottom = 7,
48 .left = 10, .right = 10
49 };
50 NPatchInfo uiButtonHovered = {
51 .layout = NPATCH_NINE_PATCH,
52 .source = {193, 43, 32, 20},
53 .top = 7, .bottom = 7,
54 .left = 10, .right = 10
55 };
56 NPatchInfo uiButtonPressed = {
57 .layout = NPATCH_NINE_PATCH,
58 .source = {193, 64, 32, 20},
59 .top = 7, .bottom = 7,
60 .left = 10, .right = 10
61 };
62 Rectangle uiDiamondMarker = {145, 48, 15, 15};
63
64 Level loadedLevels[32] = {0};
65 Level levels[] = {
66 [0] = {
67 .state = LEVEL_STATE_BUILDING,
68 .initialGold = 500,
69 .waves[0] = {
70 .enemyType = ENEMY_TYPE_SHIELD,
71 .wave = 0,
72 .count = 1,
73 .interval = 2.5f,
74 .delay = 1.0f,
75 .spawnPosition = {2, 6},
76 },
77 .waves[1] = {
78 .enemyType = ENEMY_TYPE_RUNNER,
79 .wave = 0,
80 .count = 5,
81 .interval = 0.5f,
82 .delay = 1.0f,
83 .spawnPosition = {-2, 6},
84 },
85 .waves[2] = {
86 .enemyType = ENEMY_TYPE_SHIELD,
87 .wave = 1,
88 .count = 20,
89 .interval = 1.5f,
90 .delay = 1.0f,
91 .spawnPosition = {0, 6},
92 },
93 .waves[3] = {
94 .enemyType = ENEMY_TYPE_MINION,
95 .wave = 2,
96 .count = 30,
97 .interval = 1.2f,
98 .delay = 1.0f,
99 .spawnPosition = {2, 6},
100 },
101 .waves[4] = {
102 .enemyType = ENEMY_TYPE_BOSS,
103 .wave = 2,
104 .count = 2,
105 .interval = 5.0f,
106 .delay = 2.0f,
107 .spawnPosition = {-2, 4},
108 }
109 },
110 };
111
112 Level *currentLevel = levels;
113
114 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
115 int LoadConfig();
116
117 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
118 {
119 int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
120 int panelWidth = textWidth + 40;
121 int posX = anchorX - panelWidth * alignX;
122 int textOffset = 20;
123 DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
124 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
125 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
126 }
127
128 void DrawTitle(const char *text)
129 {
130 DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
131 }
132
133 //# Game
134
135 static Model LoadGLBModel(char *filename)
136 {
137 Model model = LoadModel(TextFormat("data/%s.glb",filename));
138 for (int i = 0; i < model.materialCount; i++)
139 {
140 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
141 }
142 return model;
143 }
144
145 void LoadAssets()
146 {
147 // load a sprite sheet that contains all units
148 spriteSheet = LoadTexture("data/spritesheet.png");
149 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
150
151 // we'll use a palette texture to colorize the all buildings and environment art
152 palette = LoadTexture("data/palette.png");
153 // The texture uses gradients on very small space, so we'll enable bilinear filtering
154 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
155
156 gameFontNormal = LoadFont("data/alagard.png");
157
158 floorTileAModel = LoadGLBModel("floor-tile-a");
159 floorTileBModel = LoadGLBModel("floor-tile-b");
160 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
161 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
162 firTreeModel[0] = LoadGLBModel("firtree-1-a");
163 firTreeModel[1] = LoadGLBModel("firtree-1-b");
164 rockModels[0] = LoadGLBModel("rock-1");
165 rockModels[1] = LoadGLBModel("rock-2");
166 rockModels[2] = LoadGLBModel("rock-3");
167 rockModels[3] = LoadGLBModel("rock-4");
168 rockModels[4] = LoadGLBModel("rock-5");
169 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
170
171 pathArrowModel = LoadGLBModel("direction-arrow-x");
172 greenArrowModel = LoadGLBModel("green-arrow");
173
174 TowerLoadAssets();
175 }
176
177 void InitLevel(Level *level)
178 {
179 level->seed = (int)(GetTime() * 100.0f);
180
181 TowerInit();
182 EnemyInit();
183 ProjectileInit();
184 ParticleInit();
185 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
186
187 level->placementMode = 0;
188 level->state = LEVEL_STATE_BUILDING;
189 level->nextState = LEVEL_STATE_NONE;
190 level->playerGold = level->initialGold;
191 level->currentWave = 0;
192 level->placementX = -1;
193 level->placementY = 0;
194
195 Camera *camera = &level->camera;
196 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
197 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
198 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
199 camera->fovy = 11.5f;
200 camera->projection = CAMERA_ORTHOGRAPHIC;
201 }
202
203 void DrawLevelHud(Level *level)
204 {
205 const char *text = TextFormat("Gold: %d", level->playerGold);
206 DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
207 }
208
209 void DrawLevelReportLostWave(Level *level)
210 {
211 BeginMode3D(level->camera);
212 DrawLevelGround(level);
213 TowerUpdateAllRangeFade(0, 0.0f);
214 TowerDrawAll();
215 EnemyDraw();
216 ProjectileDraw();
217 ParticleDraw();
218 guiState.isBlocked = 0;
219 EndMode3D();
220
221 TowerDrawAllHealthBars(level->camera);
222
223 DrawTitle("Wave lost");
224
225 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
226 {
227 level->nextState = LEVEL_STATE_RESET;
228 }
229 }
230
231 int HasLevelNextWave(Level *level)
232 {
233 for (int i = 0; i < 10; i++)
234 {
235 EnemyWave *wave = &level->waves[i];
236 if (wave->wave == level->currentWave)
237 {
238 return 1;
239 }
240 }
241 return 0;
242 }
243
244 void DrawLevelReportWonWave(Level *level)
245 {
246 BeginMode3D(level->camera);
247 DrawLevelGround(level);
248 TowerUpdateAllRangeFade(0, 0.0f);
249 TowerDrawAll();
250 EnemyDraw();
251 ProjectileDraw();
252 ParticleDraw();
253 guiState.isBlocked = 0;
254 EndMode3D();
255
256 TowerDrawAllHealthBars(level->camera);
257
258 DrawTitle("Wave won");
259
260
261 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
262 {
263 level->nextState = LEVEL_STATE_RESET;
264 }
265
266 if (HasLevelNextWave(level))
267 {
268 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
269 {
270 level->nextState = LEVEL_STATE_BUILDING;
271 }
272 }
273 else {
274 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
275 {
276 level->nextState = LEVEL_STATE_WON_LEVEL;
277 }
278 }
279 }
280
281 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
282 {
283 static ButtonState buttonStates[8] = {0};
284 int cost = TowerTypeGetCosts(towerType);
285 const char *text = TextFormat("%s: %d", name, cost);
286 buttonStates[towerType].isSelected = level->placementMode == towerType;
287 buttonStates[towerType].isDisabled = level->playerGold < cost;
288 if (Button(text, x, y, width, height, &buttonStates[towerType]))
289 {
290 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
291 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
292 return 1;
293 }
294 return 0;
295 }
296
297 float GetRandomFloat(float min, float max)
298 {
299 int random = GetRandomValue(0, 0xfffffff);
300 return ((float)random / (float)0xfffffff) * (max - min) + min;
301 }
302
303 void DrawLevelGround(Level *level)
304 {
305 // draw checkerboard ground pattern
306 for (int x = -5; x <= 5; x += 1)
307 {
308 for (int y = -5; y <= 5; y += 1)
309 {
310 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
311 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
312 }
313 }
314
315 int oldSeed = GetRandomValue(0, 0xfffffff);
316 SetRandomSeed(level->seed);
317 // increase probability for trees via duplicated entries
318 Model borderModels[64];
319 int maxRockCount = GetRandomValue(2, 6);
320 int maxTreeCount = GetRandomValue(10, 20);
321 int maxFirTreeCount = GetRandomValue(5, 10);
322 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
323 int grassPatchCount = GetRandomValue(5, 30);
324
325 int modelCount = 0;
326 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
327 {
328 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
329 }
330 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
331 {
332 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
333 }
334 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
335 {
336 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
337 }
338 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
339 {
340 borderModels[modelCount++] = grassPatchModel[0];
341 }
342
343 // draw some objects around the border of the map
344 Vector3 up = {0, 1, 0};
345 // a pseudo random number generator to get the same result every time
346 const float wiggle = 0.75f;
347 const int layerCount = 3;
348 for (int layer = 0; layer <= layerCount; layer++)
349 {
350 int layerPos = 6 + layer;
351 Model *selectedModels = borderModels;
352 int selectedModelCount = modelCount;
353 if (layer == 0)
354 {
355 selectedModels = grassPatchModel;
356 selectedModelCount = 1;
357 }
358 for (int x = -6 - layer; x <= 6 + layer; x += 1)
359 {
360 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
361 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
362 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
363 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
364 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
365 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
366 }
367
368 for (int z = -5 - layer; z <= 5 + layer; z += 1)
369 {
370 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
371 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
372 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
373 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)],
374 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
375 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
376 }
377 }
378
379 SetRandomSeed(oldSeed);
380 }
381
382 void DrawEnemyPath(Level *level, Color arrowColor)
383 {
384 const int castleX = 0, castleY = 0;
385 const int maxWaypointCount = 200;
386 const float timeStep = 1.0f;
387 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
388
389 // we start with a time offset to simulate the path,
390 // this way the arrows are animated in a forward moving direction
391 // The time is wrapped around the time step to get a smooth animation
392 float timeOffset = fmodf(GetTime(), timeStep);
393
394 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
395 {
396 EnemyWave *wave = &level->waves[i];
397 if (wave->wave != level->currentWave)
398 {
399 continue;
400 }
401
402 // use this dummy enemy to simulate the path
403 Enemy dummy = {
404 .enemyType = ENEMY_TYPE_MINION,
405 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
406 .nextX = wave->spawnPosition.x,
407 .nextY = wave->spawnPosition.y,
408 .currentX = wave->spawnPosition.x,
409 .currentY = wave->spawnPosition.y,
410 };
411
412 float deltaTime = timeOffset;
413 for (int j = 0; j < maxWaypointCount; j++)
414 {
415 int waypointPassedCount = 0;
416 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
417 // after the initial variable starting offset, we use a fixed time step
418 deltaTime = timeStep;
419 dummy.simPosition = pos;
420
421 // Update the dummy's position just like we do in the regular enemy update loop
422 for (int k = 0; k < waypointPassedCount; k++)
423 {
424 dummy.currentX = dummy.nextX;
425 dummy.currentY = dummy.nextY;
426 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
427 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
428 {
429 break;
430 }
431 }
432 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
433 {
434 break;
435 }
436
437 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
438 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
439 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
440 }
441 }
442 }
443
444 void DrawEnemyPaths(Level *level)
445 {
446 // disable depth testing for the path arrows
447 // flush the 3D batch to draw the arrows on top of everything
448 rlDrawRenderBatchActive();
449 rlDisableDepthTest();
450 DrawEnemyPath(level, (Color){64, 64, 64, 160});
451
452 rlDrawRenderBatchActive();
453 rlEnableDepthTest();
454 DrawEnemyPath(level, WHITE);
455 }
456
457 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
458 {
459 float dt = gameTime.fixedDeltaTime;
460 // smooth transition for the placement position using exponential decay
461 const float lambda = 15.0f;
462 float factor = 1.0f - expf(-lambda * dt);
463
464 float damping = 0.5f;
465 float springStiffness = 300.0f;
466 float springDecay = 95.0f;
467 float minHeight = 0.35f;
468
469 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
470 {
471 damping = 1.0f;
472 springDecay = 90.0f;
473 springStiffness = 100.0f;
474 minHeight = 0.70f;
475 }
476
477 for (int i = 0; i < gameTime.fixedStepCount; i++)
478 {
479 level->placementTransitionPosition =
480 Vector2Lerp(
481 level->placementTransitionPosition,
482 (Vector2){mapX, mapY}, factor);
483
484 // draw the spring position for debugging the spring simulation
485 // first step: stiff spring, no simulation
486 Vector3 worldPlacementPosition = (Vector3){
487 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
488 Vector3 springTargetPosition = (Vector3){
489 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
490 // consider the current velocity to predict the future position in order to dampen
491 // the spring simulation. Longer prediction times will result in more damping
492 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position,
493 Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
494 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
495 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
496 // decay velocity of the upright forcing spring
497 // This force acts like a 2nd spring that pulls the tip upright into the air above the
498 // base position
499 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
500 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
501
502 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
503 // we use a simple spring model with a rest length of 1.0f
504 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
505 float springLength = Vector3Length(springDelta);
506 float springForce = (springLength - 1.0f) * springStiffness;
507 Vector3 springForceVector = Vector3Normalize(springDelta);
508 springForceVector = Vector3Scale(springForceVector, springForce);
509 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity,
510 Vector3Scale(springForceVector, dt));
511
512 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
513 Vector3Scale(level->placementTowerSpring.velocity, dt));
514 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
515 {
516 level->placementTowerSpring.velocity.y *= -1.0f;
517 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
518 }
519 }
520 }
521
522 void DrawLevelBuildingPlacementState(Level *level)
523 {
524 const float placementDuration = 0.5f;
525
526 level->placementTimer += gameTime.deltaTime;
527 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
528 {
529 level->placementPhase = PLACEMENT_PHASE_MOVING;
530 level->placementTimer = 0.0f;
531 }
532
533 BeginMode3D(level->camera);
534 DrawLevelGround(level);
535
536 int blockedCellCount = 0;
537 Vector2 blockedCells[1];
538 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
539 float planeDistance = ray.position.y / -ray.direction.y;
540 float planeX = ray.direction.x * planeDistance + ray.position.x;
541 float planeY = ray.direction.z * planeDistance + ray.position.z;
542 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
543 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
544 if (level->placementPhase == PLACEMENT_PHASE_MOVING &&
545 level->placementMode && !guiState.isBlocked &&
546 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
547 {
548 level->placementX = mapX;
549 level->placementY = mapY;
550 }
551 else
552 {
553 mapX = level->placementX;
554 mapY = level->placementY;
555 }
556 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
557 PathFindingMapUpdate(blockedCellCount, blockedCells);
558
559 TowerUpdateAllRangeFade(0, 0.0f);
560 TowerDrawAll();
561 EnemyDraw();
562 ProjectileDraw();
563 ParticleDraw();
564 DrawEnemyPaths(level);
565
566 // let the tower float up and down. Consider this height in the spring simulation as well
567 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
568
569 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
570 {
571 // The bouncing spring needs a bit of outro time to look nice and complete.
572 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
573 // and the last 1/3rd is the outro physics (bouncing)
574 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
575 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
576 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
577 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
578 towerFloatHeight = linearBlendHeight + parabola;
579 }
580
581 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
582
583 rlPushMatrix();
584 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
585
586 // calculate x and z rotation to align the model with the spring
587 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
588 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
589 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
590 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
591 float springLength = Vector3Length(towerUp);
592 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
593 float towerSquash = 1.0f / towerStretch;
594
595 Tower dummy = {
596 .towerType = level->placementMode,
597 };
598
599 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
600 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
601 {
602 rangeAlpha = 1.0f - rangeAlpha;
603 }
604 else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
605 {
606 rangeAlpha = 1.0f;
607 }
608
609 TowerDrawRange(&dummy, rangeAlpha);
610
611 rlPushMatrix();
612 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
613
614 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
615 rlScalef(towerSquash, towerStretch, towerSquash);
616 TowerDrawModel(&dummy);
617 rlPopMatrix();
618
619
620 // draw a shadow for the tower
621 float umbrasize = 0.8 + sqrtf(towerFloatHeight);
622 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
623 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
624
625
626 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
627 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
628 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
629 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
630
631 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
632 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
633 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
634 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
635 rlPopMatrix();
636
637 guiState.isBlocked = 0;
638
639 EndMode3D();
640
641 TowerDrawAllHealthBars(level->camera);
642
643 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
644 {
645 if (level->placementTimer > placementDuration)
646 {
647 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
648 // testing repairing
649 tower->damage = 2.5f;
650 level->playerGold -= TowerTypeGetCosts(level->placementMode);
651 level->nextState = LEVEL_STATE_BUILDING;
652 level->placementMode = TOWER_TYPE_NONE;
653 }
654 }
655 else
656 {
657 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
658 {
659 level->nextState = LEVEL_STATE_BUILDING;
660 level->placementMode = TOWER_TYPE_NONE;
661 TraceLog(LOG_INFO, "Cancel building");
662 }
663
664 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
665 {
666 level->placementPhase = PLACEMENT_PHASE_PLACING;
667 level->placementTimer = 0.0f;
668 }
669 }
670 }
671
672 enum ContextMenuType
673 {
674 CONTEXT_MENU_TYPE_MAIN,
675 CONTEXT_MENU_TYPE_SELL_CONFIRM,
676 CONTEXT_MENU_TYPE_UPGRADE,
677 };
678
679 enum UpgradeType
680 {
681 UPGRADE_TYPE_SPEED,
682 UPGRADE_TYPE_DAMAGE,
683 UPGRADE_TYPE_RANGE,
684 };
685
686 typedef struct ContextMenuArgs
687 {
688 void *data;
689 uint8_t uint8;
690 int32_t int32;
691 Tower *tower;
692 } ContextMenuArgs;
693
694 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
695 {
696 uint8_t towerType = data->uint8;
697 level->placementMode = towerType;
698 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
699 return 1;
700 }
701
702 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
703 {
704 Tower *tower = data->tower;
705 int gold = data->int32;
706 level->playerGold += gold;
707 tower->towerType = TOWER_TYPE_NONE;
708 return 1;
709 }
710
711 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
712 {
713 return 1;
714 }
715
716 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
717 {
718 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
719 return 0;
720 }
721
722 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
723 {
724 Tower *tower = data->tower;
725 switch (data->uint8)
726 {
727 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
728 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
729 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
730 }
731 level->playerGold -= data->int32;
732 return 0;
733 }
734
735 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
736 {
737 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
738 return 0;
739 }
740
741 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
742 {
743 Tower *tower = data->tower;
744 if (level->playerGold >= 1)
745 {
746 level->playerGold -= 1;
747 tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
748 }
749 return tower->damage == 0.0f;
750 }
751
752 typedef struct ContextMenuItem
753 {
754 uint8_t index;
755 char text[24];
756 float alignX;
757 int (*action)(Level*, ContextMenuArgs*);
758 void *data;
759 ContextMenuArgs args;
760 ButtonState buttonState;
761 } ContextMenuItem;
762
763 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
764 {
765 ContextMenuItem item = {.index = index, .alignX = alignX};
766 strncpy(item.text, text, 23);
767 item.text[23] = 0;
768 return item;
769 }
770
771 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
772 {
773 ContextMenuItem item = {.index = index, .action = action, .args = args};
774 strncpy(item.text, text, 23);
775 item.text[23] = 0;
776 return item;
777 }
778
779 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
780 {
781 const int itemHeight = 28;
782 const int itemSpacing = 1;
783 const int padding = 8;
784 int itemCount = 0;
785 for (int i = 0; menus[i].text[0]; i++)
786 {
787 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
788 }
789
790 Rectangle contextMenu = {0, 0, width,
791 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
792
793 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
794 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
795
796 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
797 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
798 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
799 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
800
801 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
802 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
803 const int itemX = contextMenu.x + itemSpacing;
804 const int itemWidth = contextMenu.width - itemSpacing * 2;
805 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
806 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
807 int status = 0;
808 for (int i = 0; menus[i].text[0]; i++)
809 {
810 if (menus[i].action)
811 {
812 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
813 {
814 status = menus[i].action(level, &menus[i].args);
815 }
816 }
817 else
818 {
819 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
820 }
821 }
822
823 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
824 {
825 return 1;
826 }
827
828 return status;
829 }
830
831 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
832 {
833 ContextMenuItem menu[12] = {0};
834 int menuCount = 0;
835 int menuIndex = 0;
836 if (tower)
837 {
838
839 if (tower) {
840 menu[menuCount++] = ContextMenuItemText(menuIndex++, TowerTypeGetName(tower->towerType), 0.5f);
841 }
842
843 // two texts, same line
844 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
845 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
846
847 if (tower->towerType != TOWER_TYPE_BASE)
848 {
849 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade,
850 (ContextMenuArgs){.tower = tower});
851 }
852
853 if (tower->towerType != TOWER_TYPE_BASE)
854 {
855
856 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell,
857 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
858 }
859 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
860 {
861 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair,
862 (ContextMenuArgs){.tower = tower});
863 }
864 }
865 else
866 {
867 menu[menuCount] = ContextMenuItemButton(menuIndex++,
868 TextFormat("Wall: %dG", TowerTypeGetCosts(TOWER_TYPE_WALL)),
869 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
870 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_WALL);
871
872 menu[menuCount] = ContextMenuItemButton(menuIndex++,
873 TextFormat("Archer: %dG", TowerTypeGetCosts(TOWER_TYPE_ARCHER)),
874 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
875 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_ARCHER);
876
877 menu[menuCount] = ContextMenuItemButton(menuIndex++,
878 TextFormat("Ballista: %dG", TowerTypeGetCosts(TOWER_TYPE_BALLISTA)),
879 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
880 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_BALLISTA);
881
882 menu[menuCount] = ContextMenuItemButton(menuIndex++,
883 TextFormat("Catapult: %dG", TowerTypeGetCosts(TOWER_TYPE_CATAPULT)),
884 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
885 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_CATAPULT);
886 }
887
888 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
889 {
890 level->placementContextMenuStatus = -1;
891 }
892 }
893
894 void DrawLevelBuildingState(Level *level)
895 {
896 // when the context menu is not active, we update the placement position
897 if (level->placementContextMenuStatus == 0)
898 {
899 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
900 float hitDistance = ray.position.y / -ray.direction.y;
901 float hitX = ray.direction.x * hitDistance + ray.position.x;
902 float hitY = ray.direction.z * hitDistance + ray.position.z;
903 level->placementX = (int)floorf(hitX + 0.5f);
904 level->placementY = (int)floorf(hitY + 0.5f);
905 }
906
907 // the currently hovered/selected tower
908 Tower *tower = TowerGetAt(level->placementX, level->placementY);
909 // show the range of the tower when hovering/selecting it
910 TowerUpdateAllRangeFade(tower, 0.0f);
911
912 BeginMode3D(level->camera);
913 DrawLevelGround(level);
914 PathFindingMapUpdate(0, 0);
915 TowerDrawAll();
916 EnemyDraw();
917 ProjectileDraw();
918 ParticleDraw();
919 DrawEnemyPaths(level);
920
921 guiState.isBlocked = 0;
922
923 // Hover rectangle, when the mouse is over the map
924 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
925 if (isHovering)
926 {
927 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
928 }
929
930 EndMode3D();
931
932 TowerDrawAllHealthBars(level->camera);
933
934 DrawTitle("Building phase");
935
936 // Draw the context menu when the context menu is active
937 if (level->placementContextMenuStatus >= 1)
938 {
939 float maxHitpoints = 0.0f;
940 float hp = 0.0f;
941 float damageFactor = 0.0f;
942 int32_t sellValue = 0;
943
944 if (tower)
945 {
946 maxHitpoints = TowerGetMaxHealth(tower);
947 hp = maxHitpoints - tower->damage;
948 damageFactor = 1.0f - tower->damage / maxHitpoints;
949 sellValue = (int32_t) ceilf(TowerTypeGetCosts(tower->towerType) * 0.5f * damageFactor);
950 }
951
952 ContextMenuItem menu[12] = {0};
953 int menuCount = 0;
954 int menuIndex = 0;
955 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
956 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
957
958 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
959 {
960 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
961 }
962 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
963 {
964 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
965 int costs = totalLevel * 4;
966 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
967 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
968 TowerTypeGetName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
969 int buttonMenuIndex = menuIndex;
970 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs),
971 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
972 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
973 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
974 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
975 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
976
977 // check if buttons should be disabled
978 if (isMaxLevel || level->playerGold < costs)
979 {
980 for (int i = buttonMenuIndex; i < menuCount; i++)
981 {
982 menu[i].buttonState.isDisabled = 1;
983 }
984 }
985
986 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
987 {
988 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
989 }
990 }
991 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
992 {
993 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", TowerTypeGetName(tower->towerType)), 0.5f);
994 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm,
995 (ContextMenuArgs){.tower = tower, .int32 = sellValue});
996 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
997 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
998 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
999 {
1000 level->placementContextMenuStatus = -1;
1001 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
1002 }
1003 }
1004 }
1005
1006 // Activate the context menu when the mouse is clicked and the context menu is not active
1007 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
1008 {
1009 level->placementContextMenuStatus += 1;
1010 }
1011
1012 if (level->placementContextMenuStatus == 0)
1013 {
1014 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1015 {
1016 level->nextState = LEVEL_STATE_RESET;
1017 }
1018
1019 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1020 {
1021 level->nextState = LEVEL_STATE_BATTLE;
1022 }
1023
1024 }
1025 }
1026
1027 void InitBattleStateConditions(Level *level)
1028 {
1029 level->state = LEVEL_STATE_BATTLE;
1030 level->nextState = LEVEL_STATE_NONE;
1031 level->waveEndTimer = 0.0f;
1032 for (int i = 0; i < 10; i++)
1033 {
1034 EnemyWave *wave = &level->waves[i];
1035 wave->spawned = 0;
1036 wave->timeToSpawnNext = wave->delay;
1037 }
1038 }
1039
1040 void DrawLevelBattleState(Level *level)
1041 {
1042 BeginMode3D(level->camera);
1043 DrawLevelGround(level);
1044 TowerUpdateAllRangeFade(0, 0.0f);
1045 TowerDrawAll();
1046 EnemyDraw();
1047 ProjectileDraw();
1048 ParticleDraw();
1049 guiState.isBlocked = 0;
1050 EndMode3D();
1051
1052 EnemyDrawHealthbars(level->camera);
1053 TowerDrawAllHealthBars(level->camera);
1054
1055 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1056 {
1057 level->nextState = LEVEL_STATE_RESET;
1058 }
1059
1060 int maxCount = 0;
1061 int remainingCount = 0;
1062 for (int i = 0; i < 10; i++)
1063 {
1064 EnemyWave *wave = &level->waves[i];
1065 if (wave->wave != level->currentWave)
1066 {
1067 continue;
1068 }
1069 maxCount += wave->count;
1070 remainingCount += wave->count - wave->spawned;
1071 }
1072 int aliveCount = EnemyCount();
1073 remainingCount += aliveCount;
1074
1075 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1076 DrawTitle(text);
1077 }
1078
1079 void DrawLevel(Level *level)
1080 {
1081 switch (level->state)
1082 {
1083 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1084 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1085 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1086 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1087 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1088 default: break;
1089 }
1090
1091 DrawLevelHud(level);
1092 }
1093
1094 EMSCRIPTEN_KEEPALIVE
1095 void RequestReload()
1096 {
1097 currentLevel->nextState = LEVEL_STATE_RELOAD;
1098 }
1099
1100 void UpdateLevel(Level *level)
1101 {
1102 if (level->state == LEVEL_STATE_BATTLE)
1103 {
1104 int activeWaves = 0;
1105 for (int i = 0; i < 10; i++)
1106 {
1107 EnemyWave *wave = &level->waves[i];
1108 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1109 {
1110 continue;
1111 }
1112 activeWaves++;
1113 wave->timeToSpawnNext -= gameTime.deltaTime;
1114 if (wave->timeToSpawnNext <= 0.0f)
1115 {
1116 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1117 if (enemy)
1118 {
1119 wave->timeToSpawnNext = wave->interval;
1120 wave->spawned++;
1121 }
1122 }
1123 }
1124 if (TowerGetByType(TOWER_TYPE_BASE) == 0) {
1125 level->waveEndTimer += gameTime.deltaTime;
1126 if (level->waveEndTimer >= 2.0f)
1127 {
1128 level->nextState = LEVEL_STATE_LOST_WAVE;
1129 }
1130 }
1131 else if (activeWaves == 0 && EnemyCount() == 0)
1132 {
1133 level->waveEndTimer += gameTime.deltaTime;
1134 if (level->waveEndTimer >= 2.0f)
1135 {
1136 level->nextState = LEVEL_STATE_WON_WAVE;
1137 }
1138 }
1139 }
1140
1141 PathFindingMapUpdate(0, 0);
1142 EnemyUpdate();
1143 TowerUpdate();
1144 ProjectileUpdate();
1145 ParticleUpdate();
1146
1147 if (level->nextState == LEVEL_STATE_RELOAD)
1148 {
1149 if (LoadConfig())
1150 {
1151 level->nextState = LEVEL_STATE_RESET;
1152 }
1153 else
1154 {
1155 level->nextState = level->state;
1156 }
1157 }
1158
1159 if (level->nextState == LEVEL_STATE_RESET)
1160 {
1161 currentLevel = loadedLevels[0].initialGold > 0 ? loadedLevels : currentLevel;
1162 TraceLog(LOG_INFO, "Using level with initialGold = %d", currentLevel->initialGold);
1163
1164 InitLevel(level);
1165 }
1166
1167 if (level->nextState == LEVEL_STATE_BATTLE)
1168 {
1169 InitBattleStateConditions(level);
1170 }
1171
1172 if (level->nextState == LEVEL_STATE_WON_WAVE)
1173 {
1174 level->currentWave++;
1175 level->state = LEVEL_STATE_WON_WAVE;
1176 }
1177
1178 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1179 {
1180 level->state = LEVEL_STATE_LOST_WAVE;
1181 }
1182
1183 if (level->nextState == LEVEL_STATE_BUILDING)
1184 {
1185 level->state = LEVEL_STATE_BUILDING;
1186 level->placementContextMenuStatus = 0;
1187 }
1188
1189 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1190 {
1191 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1192 level->placementTransitionPosition = (Vector2){
1193 level->placementX, level->placementY};
1194 // initialize the spring to the current position
1195 level->placementTowerSpring = (PhysicsPoint){
1196 .position = (Vector3){level->placementX, 8.0f, level->placementY},
1197 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1198 };
1199 level->placementPhase = PLACEMENT_PHASE_STARTING;
1200 level->placementTimer = 0.0f;
1201 }
1202
1203 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1204 {
1205 // make something of this later
1206 InitLevel(level);
1207 }
1208
1209 level->nextState = LEVEL_STATE_NONE;
1210 }
1211
1212 float nextSpawnTime = 0.0f;
1213
1214 int LoadConfig()
1215 {
1216 char *config = LoadFileText("data/level.txt");
1217 if (!config)
1218 {
1219 TraceLog(LOG_ERROR, "Failed to load level config");
1220 return 0;
1221 }
1222
1223 ParsedGameData gameData = {0};
1224 if (ParseGameData(&gameData, config))
1225 {
1226 for (int i = 0; i < 8; i++)
1227 {
1228 EnemyClassConfig *enemyClassConfig = &gameData.enemyClasses[i];
1229 if (enemyClassConfig->health > 0.0f)
1230 {
1231 enemyClassConfigs[i] = *enemyClassConfig;
1232 }
1233 }
1234
1235 for (int i = 0; i < 32; i++)
1236 {
1237 Level *level = &gameData.levels[i];
1238 if (level->initialGold > 0)
1239 {
1240 loadedLevels[i] = *level;
1241 }
1242 }
1243
1244 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
1245 {
1246 TowerTypeConfig *towerTypeConfig = &gameData.towerTypes[i];
1247 if (towerTypeConfig->maxHealth > 0)
1248 {
1249 TowerTypeSetData(i, towerTypeConfig);
1250 }
1251 }
1252
1253 currentLevel = loadedLevels[0].initialGold > 0 ? loadedLevels : levels;
1254 } else {
1255 TraceLog(LOG_ERROR, "Parsing error: %s", gameData.parseError);
1256 }
1257
1258 UnloadFileText(config);
1259
1260 return gameData.parseError == 0;
1261 }
1262
1263 void InitGame()
1264 {
1265 TowerInit();
1266 EnemyInit();
1267 ProjectileInit();
1268 ParticleInit();
1269 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1270
1271 InitLevel(currentLevel);
1272 }
1273
1274 //# Immediate GUI functions
1275
1276 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1277 {
1278 const float healthBarHeight = 6.0f;
1279 const float healthBarOffset = 15.0f;
1280 const float inset = 2.0f;
1281 const float innerWidth = healthBarWidth - inset * 2;
1282 const float innerHeight = healthBarHeight - inset * 2;
1283
1284 Vector2 screenPos = GetWorldToScreen(position, camera);
1285 screenPos = Vector2Add(screenPos, screenOffset);
1286 float centerX = screenPos.x - healthBarWidth * 0.5f;
1287 float topY = screenPos.y - healthBarOffset;
1288 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1289 float healthWidth = innerWidth * healthRatio;
1290 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1291 }
1292
1293 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1294 {
1295 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1296
1297 DrawTextEx(gameFontNormal, text, (Vector2){
1298 x + (width - textSize.x) * alignX,
1299 y + (height - textSize.y) * alignY
1300 }, gameFontNormal.baseSize, 1, textColor);
1301 }
1302
1303 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1304 {
1305 Rectangle bounds = {x, y, width, height};
1306 int isPressed = 0;
1307 int isSelected = state && state->isSelected;
1308 int isDisabled = state && state->isDisabled;
1309 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1310 {
1311 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1312 {
1313 isPressed = 1;
1314 }
1315 guiState.isBlocked = 1;
1316 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1317 bounds, Vector2Zero(), 0, WHITE);
1318 }
1319 else
1320 {
1321 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1322 bounds, Vector2Zero(), 0, WHITE);
1323 }
1324 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1325 Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1326 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1327 return isPressed;
1328 }
1329
1330 //# Main game loop
1331
1332 void GameUpdate()
1333 {
1334 UpdateLevel(currentLevel);
1335 }
1336
1337 #ifdef PLATFORM_WEB
1338 #include <string.h>
1339 #include <stdio.h>
1340 #include <stdlib.h>
1341 void LogToWeb(int logLevel, const char *text, va_list args)
1342 {
1343 char logBuffer[1024] = {0};
1344 vsnprintf(logBuffer, 1024, text, args);
1345 char escapedBuffer[2048] = {0};
1346 // escape single quotes
1347 int outIndex = 0;
1348 for (int i = 0; i < 1024; i++)
1349 {
1350 if (logBuffer[i] == '\'')
1351 {
1352 escapedBuffer[outIndex++] = '\\';
1353 }
1354 escapedBuffer[outIndex++] = logBuffer[i];
1355 }
1356 char js[4096] = {0};
1357 snprintf(js, 4096, "Module.LogMessage(%d, '%s');", logLevel, escapedBuffer);
1358 emscripten_run_script(js);
1359 }
1360
1361 void InitWeb()
1362 {
1363 // create button that adds a textarea with the data/level.txt content
1364 // together with a button to load the data
1365 char *js = LoadFileText("data/html-edit.js");
1366 emscripten_run_script(js);
1367 UnloadFileText(js);
1368 TraceLog(LOG_INFO, "Loaded html-edit.js");
1369
1370 SetTraceLogCallback(LogToWeb);
1371 TraceLog(LOG_INFO, "JS Logger set");
1372 }
1373 #else
1374 void InitWeb()
1375 {
1376 }
1377 #endif
1378
1379 int main(void)
1380 {
1381 InitWeb();
1382 int screenWidth, screenHeight;
1383 GetPreferredSize(&screenWidth, &screenHeight);
1384 InitWindow(screenWidth, screenHeight, "Tower defense");
1385 float gamespeed = 1.0f;
1386 int frameRate = 30;
1387 SetTargetFPS(30);
1388
1389 LoadAssets();
1390 LoadConfig();
1391 InitGame();
1392
1393
1394
1395 float pause = 1.0f;
1396
1397 while (!WindowShouldClose())
1398 {
1399 if (IsPaused()) {
1400 // canvas is not visible in browser - do nothing
1401 continue;
1402 }
1403
1404 if (IsKeyPressed(KEY_F))
1405 {
1406 frameRate = (frameRate + 5) % 30;
1407 frameRate = frameRate < 10 ? 10 : frameRate;
1408 SetTargetFPS(frameRate);
1409 }
1410
1411 if (IsKeyPressed(KEY_T))
1412 {
1413 gamespeed += 0.1f;
1414 if (gamespeed > 1.05f) gamespeed = 0.1f;
1415 }
1416
1417 if (IsKeyPressed(KEY_P))
1418 {
1419 pause = pause > 0.5f ? 0.0f : 1.0f;
1420 }
1421
1422 float dt = GetFrameTime() * gamespeed * pause;
1423 // cap maximum delta time to 0.1 seconds to prevent large time steps
1424 if (dt > 0.1f) dt = 0.1f;
1425 gameTime.time += dt;
1426 gameTime.deltaTime = dt;
1427 gameTime.frameCount += 1;
1428
1429 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1430 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1431
1432 BeginDrawing();
1433 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1434
1435 GameUpdate();
1436 DrawLevel(currentLevel);
1437
1438 if (gamespeed != 1.0f)
1439 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1440 EndDrawing();
1441
1442 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1443 }
1444
1445 CloseWindow();
1446
1447 return 0;
1448 }
1 #include "td_main.h"
2 #include <raylib.h>
3 #include <stdio.h>
4 #include <string.h>
5
6 typedef struct ParserState
7 {
8 char *input;
9 int position;
10 char nextToken[256];
11 } ParserState;
12
13 int ParserStateGetLineNumber(ParserState *state)
14 {
15 int lineNumber = 1;
16 for (int i = 0; i < state->position; i++)
17 {
18 if (state->input[i] == '\n')
19 {
20 lineNumber++;
21 }
22 }
23 return lineNumber;
24 }
25
26 void ParserStateSkipWhiteSpaces(ParserState *state)
27 {
28 char *input = state->input;
29 int pos = state->position;
30 int skipped = 1;
31 while (skipped)
32 {
33 skipped = 0;
34 if (input[pos] == '-' && input[pos + 1] == '-')
35 {
36 skipped = 1;
37 // skip comments
38 while (input[pos] != 0 && input[pos] != '\n')
39 {
40 pos++;
41 }
42 }
43
44 // skip white spaces and ignore colons
45 while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
46 {
47 skipped = 1;
48 pos++;
49 }
50
51 // repeat until no more white spaces or comments
52 }
53 state->position = pos;
54 }
55
56 int ParserStateReadNextToken(ParserState *state)
57 {
58 ParserStateSkipWhiteSpaces(state);
59
60 int i = 0, pos = state->position;
61 char *input = state->input;
62
63 // read token
64 while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
65 {
66 state->nextToken[i] = input[pos];
67 pos++;
68 i++;
69 }
70 state->position = pos;
71
72 if (i == 0 || i == 256)
73 {
74 state->nextToken[0] = 0;
75 return 0;
76 }
77 // terminate the token
78 state->nextToken[i] = 0;
79 return 1;
80 }
81
82 int ParserStateReadNextInt(ParserState *state, int *value)
83 {
84 if (!ParserStateReadNextToken(state))
85 {
86 return 0;
87 }
88 // check if the token is a valid integer
89 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
90 for (int i = isSigned; state->nextToken[i] != 0; i++)
91 {
92 if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
93 {
94 return 0;
95 }
96 }
97 *value = TextToInteger(state->nextToken);
98 return 1;
99 }
100
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103 if (!ParserStateReadNextToken(state))
104 {
105 return 0;
106 }
107 // check if the token is a valid float number
108 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109 int hasDot = 0;
110 for (int i = isSigned; state->nextToken[i] != 0; i++)
111 {
112 if (state->nextToken[i] == '.')
113 {
114 if (hasDot)
115 {
116 return 0;
117 }
118 hasDot = 1;
119 }
120 else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121 {
122 return 0;
123 }
124 }
125
126 *value = TextToFloat(state->nextToken);
127 return 1;
128 }
129
130 typedef enum TryReadResult
131 {
132 TryReadResult_NoMatch,
133 TryReadResult_Error,
134 TryReadResult_Success
135 } TryReadResult;
136
137 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
138 {
139 gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
140 return TryReadResult_Error;
141 }
142
143 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
144 {
145 if (!TextIsEqual(state->nextToken, key))
146 {
147 return TryReadResult_NoMatch;
148 }
149
150 if (!ParserStateReadNextInt(state, value))
151 {
152 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
153 }
154
155 // range test, if minRange == maxRange, we don't check the range
156 if (minRange != maxRange && (*value < minRange || *value > maxRange))
157 {
158 return ParseGameDataError(gameData, state, TextFormat(
159 "Invalid value range for %s, range is [%d, %d], value is %d",
160 key, minRange, maxRange, *value));
161 }
162
163 return TryReadResult_Success;
164 }
165
166 TryReadResult ParseGameDataTryReadKeyUInt8(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *value, uint8_t minRange, uint8_t maxRange)
167 {
168 int intValue = *value;
169 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
170 *value = (uint8_t) intValue;
171 return result;
172 }
173
174 TryReadResult ParseGameDataTryReadKeyInt16(ParsedGameData *gameData, ParserState *state, const char *key, int16_t *value, int16_t minRange, int16_t maxRange)
175 {
176 int intValue = *value;
177 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
178 *value = (int16_t) intValue;
179 return result;
180 }
181
182 TryReadResult ParseGameDataTryReadKeyUInt16(ParsedGameData *gameData, ParserState *state, const char *key, uint16_t *value, uint16_t minRange, uint16_t maxRange)
183 {
184 int intValue = *value;
185 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
186 *value = (uint16_t) intValue;
187 return result;
188 }
189
190 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key,
191 Vector2 *vector, Vector2 minRange, Vector2 maxRange)
192 {
193 if (!TextIsEqual(state->nextToken, key))
194 {
195 return TryReadResult_NoMatch;
196 }
197
198 ParserState start = *state;
199 int x = 0, y = 0;
200 int minXRange = (int)minRange.x, maxXRange = (int)maxRange.x;
201 int minYRange = (int)minRange.y, maxYRange = (int)maxRange.y;
202
203 if (!ParserStateReadNextInt(state, &x))
204 {
205 // use start position to report the error for this KEY
206 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
207 }
208
209 // range test, if minRange == maxRange, we don't check the range
210 if (minXRange != maxXRange && (x < minXRange || x > maxXRange))
211 {
212 // use current position to report the error for x value
213 return ParseGameDataError(gameData, state, TextFormat(
214 "Invalid value x range for %s, range is [%d, %d], value is %d",
215 key, minXRange, maxXRange, x));
216 }
217
218 if (!ParserStateReadNextInt(state, &y))
219 {
220 // use start position to report the error for this KEY
221 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key));
222 }
223
224 if (minYRange != maxYRange && (y < minYRange || y > maxYRange))
225 {
226 // use current position to report the error for y value
227 return ParseGameDataError(gameData, state, TextFormat(
228 "Invalid value y range for %s, range is [%d, %d], value is %d",
229 key, minYRange, maxYRange, y));
230 }
231
232 vector->x = (float)x;
233 vector->y = (float)y;
234
235 return TryReadResult_Success;
236 }
237
238 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
239 {
240 if (!TextIsEqual(state->nextToken, key))
241 {
242 return TryReadResult_NoMatch;
243 }
244
245 if (!ParserStateReadNextFloat(state, value))
246 {
247 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
248 }
249
250 // range test, if minRange == maxRange, we don't check the range
251 if (minRange != maxRange && (*value < minRange || *value > maxRange))
252 {
253 return ParseGameDataError(gameData, state, TextFormat(
254 "Invalid value range for %s, range is [%f, %f], value is %f",
255 key, minRange, maxRange, *value));
256 }
257
258 return TryReadResult_Success;
259 }
260
261 // The enumNames is a null-terminated array of strings that represent the enum values
262 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues)
263 {
264 if (!TextIsEqual(state->nextToken, key))
265 {
266 return TryReadResult_NoMatch;
267 }
268
269 if (!ParserStateReadNextToken(state))
270 {
271 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key));
272 }
273
274 for (int i = 0; enumNames[i] != 0; i++)
275 {
276 if (TextIsEqual(state->nextToken, enumNames[i]))
277 {
278 *value = enumValues[i];
279 return TryReadResult_Success;
280 }
281 }
282
283 return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key));
284 }
285
286 TryReadResult ParseGameDataTryReadKeyEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId, uint8_t minRange, uint8_t maxRange)
287 {
288 int enemyClassId = *enemyTypeId;
289 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId,
290 (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0},
291 (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS});
292 if (minRange != maxRange)
293 {
294 enemyClassId = enemyClassId < minRange ? minRange : enemyClassId;
295 enemyClassId = enemyClassId > maxRange ? maxRange : enemyClassId;
296 }
297 *enemyTypeId = (uint8_t) enemyClassId;
298 return result;
299 }
300
301 TryReadResult ParseGameDataTryReadKeyTowerTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
302 {
303 int towerType = *towerTypeId;
304 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType,
305 (const char *[]){"TOWER_TYPE_BASE", "TOWER_TYPE_ARCHER", "TOWER_TYPE_BALLISTA", "TOWER_TYPE_CATAPULT", "TOWER_TYPE_WALL", 0},
306 (int[]){TOWER_TYPE_BASE, TOWER_TYPE_ARCHER, TOWER_TYPE_BALLISTA, TOWER_TYPE_CATAPULT, TOWER_TYPE_WALL});
307 if (minRange != maxRange)
308 {
309 towerType = towerType < minRange ? minRange : towerType;
310 towerType = towerType > maxRange ? maxRange : towerType;
311 }
312 *towerTypeId = (uint8_t) towerType;
313 return result;
314 }
315
316 TryReadResult ParseGameDataTryReadKeyProjectileTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
317 {
318 int towerType = *towerTypeId;
319 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType,
320 (const char *[]){"PROJECTILE_TYPE_ARROW", "PROJECTILE_TYPE_BALLISTA", "PROJECTILE_TYPE_CATAPULT", 0},
321 (int[]){PROJECTILE_TYPE_ARROW, PROJECTILE_TYPE_BALLISTA, PROJECTILE_TYPE_CATAPULT});
322 if (minRange != maxRange)
323 {
324 towerType = towerType < minRange ? minRange : towerType;
325 towerType = towerType > maxRange ? maxRange : towerType;
326 }
327 *towerTypeId = (uint8_t) towerType;
328 return result;
329 }
330
331
332 //----------------------------------------------------------------
333 //# Defines for compact struct field parsing
334 // A FIELDS(GENERATEr) is to be defined that will be called for each field of the struct
335 // See implementations below for how this is used
336 #define GENERATE_READFIELD_SWITCH(owner, name, type, min, max)\
337 switch (ParseGameDataTryReadKey##type(gameData, state, #name, &owner->name, min, max))\
338 {\
339 case TryReadResult_NoMatch: break;\
340 case TryReadResult_Success:\
341 if (name##Initialized) {\
342 return ParseGameDataError(gameData, state, #name " already initialized");\
343 }\
344 name##Initialized = 1;\
345 continue;\
346 case TryReadResult_Error: return TryReadResult_Error;\
347 }
348 #define GENERATE_READFIELD_SWITCH_OPTIONAL(owner, name, type, def, min, max)\
349 GENERATE_READFIELD_SWITCH(owner, name, type, min, max)
350 #define GENERATE_FIELD_INIT_DECLARATIONS(owner, name, type, min, max) int name##Initialized = 0;
351 #define GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL(owner, name, type, def, min, max) int name##Initialized = 0; owner->name = def;
352 #define GENERATE_FIELD_INIT_CHECK(owner, name, type, min, max) \
353 if (!name##Initialized) { \
354 return ParseGameDataError(gameData, state, #name " not initialized"); \
355 }
356 #define GENERATE_FIELD_INIT_CHECK_OPTIONAL(owner, name, type, def, min, max)
357
358 #define GENERATE_FIELD_PARSING \
359 FIELDS(GENERATE_FIELD_INIT_DECLARATIONS, GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL)\
360 while (1)\
361 {\
362 ParserState prevState = *state;\
363 \
364 if (!ParserStateReadNextToken(state))\
365 {\
366 /* end of file */\
367 break;\
368 }\
369 FIELDS(GENERATE_READFIELD_SWITCH, GENERATE_READFIELD_SWITCH_OPTIONAL)\
370 /* no match, return to previous state and break */\
371 *state = prevState;\
372 break;\
373 } \
374 FIELDS(GENERATE_FIELD_INIT_CHECK, GENERATE_FIELD_INIT_CHECK_OPTIONAL)\
375
376 // END OF DEFINES
377
378 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state)
379 {
380 if (!TextIsEqual(state->nextToken, "Wave"))
381 {
382 return TryReadResult_NoMatch;
383 }
384
385 Level *level = &gameData->levels[gameData->lastLevelIndex];
386 EnemyWave *wave = 0;
387 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
388 {
389 if (level->waves[i].count == 0)
390 {
391 wave = &level->waves[i];
392 break;
393 }
394 }
395
396 if (wave == 0)
397 {
398 return ParseGameDataError(gameData, state,
399 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT));
400 }
401
402 #define FIELDS(MANDATORY, OPTIONAL) \
403 MANDATORY(wave, wave, UInt8, 0, ENEMY_MAX_WAVE_COUNT - 1) \
404 MANDATORY(wave, count, UInt16, 1, 1000) \
405 MANDATORY(wave, delay, Float, 0.0f, 1000.0f) \
406 MANDATORY(wave, interval, Float, 0.0f, 1000.0f) \
407 MANDATORY(wave, spawnPosition, IntVec2, ((Vector2){-10.0f, -10.0f}), ((Vector2){10.0f, 10.0f})) \
408 MANDATORY(wave, enemyType, EnemyTypeId, 0, 0)
409
410 GENERATE_FIELD_PARSING
411 #undef FIELDS
412
413 return TryReadResult_Success;
414 }
415
416 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state)
417 {
418 uint8_t enemyClassId;
419
420 switch (ParseGameDataTryReadKeyEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId, 0, 7))
421 {
422 case TryReadResult_Success: break;
423 case TryReadResult_NoMatch: return TryReadResult_NoMatch;
424 case TryReadResult_Error: return TryReadResult_Error;
425 }
426
427 EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId];
428
429 #define FIELDS(MANDATORY, OPTIONAL) \
430 MANDATORY(enemyClass, speed, Float, 0.1f, 1000.0f) \
431 MANDATORY(enemyClass, health, Float, 1, 1000000) \
432 MANDATORY(enemyClass, radius, Float, 0.0f, 10.0f) \
433 MANDATORY(enemyClass, requiredContactTime, Float, 0.0f, 1000.0f) \
434 MANDATORY(enemyClass, maxAcceleration, Float, 0.0f, 1000.0f) \
435 MANDATORY(enemyClass, explosionDamage, Float, 0.0f, 1000.0f) \
436 MANDATORY(enemyClass, explosionRange, Float, 0.0f, 1000.0f) \
437 MANDATORY(enemyClass, explosionPushbackPower, Float, 0.0f, 1000.0f) \
438 MANDATORY(enemyClass, goldValue, Int, 1, 1000000)
439
440 GENERATE_FIELD_PARSING
441 #undef FIELDS
442
443 return TryReadResult_Success;
444 }
445
446 TryReadResult ParseGameDataTryReadTowerTypeConfigSection(ParsedGameData *gameData, ParserState *state)
447 {
448 uint8_t towerTypeId;
449
450 switch (ParseGameDataTryReadKeyTowerTypeId(gameData, state, "TowerTypeConfig", &towerTypeId, 0, TOWER_TYPE_COUNT - 1))
451 {
452 case TryReadResult_Success: break;
453 case TryReadResult_NoMatch: return TryReadResult_NoMatch;
454 case TryReadResult_Error: return TryReadResult_Error;
455 }
456
457 TowerTypeConfig *towerType = &gameData->towerTypes[towerTypeId];
458 HitEffectConfig *hitEffect = &towerType->hitEffect;
459
460 #define FIELDS(MANDATORY, OPTIONAL) \
461 MANDATORY(towerType, maxHealth, UInt16, 0, 0) \
462 OPTIONAL(towerType, cooldown, Float, 0, 0.0f, 1000.0f) \
463 OPTIONAL(towerType, maxUpgradeCooldown, Float, 0, 0.0f, 1000.0f) \
464 OPTIONAL(towerType, range, Float, 0, 0.0f, 50.0f) \
465 OPTIONAL(towerType, maxUpgradeRange, Float, 0, 0.0f, 50.0f) \
466 OPTIONAL(towerType, projectileSpeed, Float, 0, 0.0f, 100.0f) \
467 OPTIONAL(towerType, cost, UInt8, 0, 0, 255) \
468 OPTIONAL(towerType, projectileType, ProjectileTypeId, 0, 0, 32)\
469 OPTIONAL(hitEffect, damage, Float, 0, 0, 100000.0f) \
470 OPTIONAL(hitEffect, maxUpgradeDamage, Float, 0, 0, 100000.0f) \
471 OPTIONAL(hitEffect, areaDamageRadius, Float, 0, 0, 100000.0f) \
472 OPTIONAL(hitEffect, pushbackPowerDistance, Float, 0, 0, 100000.0f)
473
474 GENERATE_FIELD_PARSING
475 #undef FIELDS
476
477 return TryReadResult_Success;
478 }
479
480 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state)
481 {
482 int levelId;
483 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31);
484 if (result != TryReadResult_Success)
485 {
486 return result;
487 }
488
489 gameData->lastLevelIndex = levelId;
490 Level *level = &gameData->levels[levelId];
491
492 // since we require the initialGold to be initialized with at least 1, we can use it as a flag
493 // to detect if the level was already initialized
494 if (level->initialGold != 0)
495 {
496 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId));
497 }
498
499 int initialGoldInitialized = 0;
500
501 while (1)
502 {
503 // try to read the next token and if we don't know how to GENERATE it,
504 // we rewind and return
505 ParserState prevState = *state;
506
507 if (!ParserStateReadNextToken(state))
508 {
509 // end of file
510 break;
511 }
512
513 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000))
514 {
515 case TryReadResult_Success:
516 if (initialGoldInitialized)
517 {
518 return ParseGameDataError(gameData, state, "initialGold already initialized");
519 }
520 initialGoldInitialized = 1;
521 continue;
522 case TryReadResult_Error: return TryReadResult_Error;
523 case TryReadResult_NoMatch: break;
524 }
525
526 switch (ParseGameDataTryReadWaveSection(gameData, state))
527 {
528 case TryReadResult_Success: continue;
529 case TryReadResult_NoMatch: break;
530 case TryReadResult_Error: return TryReadResult_Error;
531 }
532
533 // no match, return to previous state and break
534 *state = prevState;
535 break;
536 }
537
538 if (!initialGoldInitialized)
539 {
540 return ParseGameDataError(gameData, state, "initialGold not initialized");
541 }
542
543 return TryReadResult_Success;
544 }
545
546 int ParseGameData(ParsedGameData *gameData, const char *input)
547 {
548 ParserState state = (ParserState){(char *)input, 0, {0}};
549 *gameData = (ParsedGameData){0};
550 gameData->lastLevelIndex = -1;
551
552 while (ParserStateReadNextToken(&state))
553 {
554 switch (ParseGameDataTryReadLevelSection(gameData, &state))
555 {
556 case TryReadResult_Success: continue;
557 case TryReadResult_Error: return 0;
558 case TryReadResult_NoMatch: break;
559 }
560
561 switch (ParseGameDataTryReadEnemyClassSection(gameData, &state))
562 {
563 case TryReadResult_Success: continue;
564 case TryReadResult_Error: return 0;
565 case TryReadResult_NoMatch: break;
566 }
567
568 switch (ParseGameDataTryReadTowerTypeConfigSection(gameData, &state))
569 {
570 case TryReadResult_Success: continue;
571 case TryReadResult_Error: return 0;
572 case TryReadResult_NoMatch: break;
573 }
574
575 // any other token is considered an error
576 ParseGameDataError(gameData, &state, TextFormat("Unexpected token: %s", state.nextToken));
577 return 0;
578 }
579
580 return 1;
581 }
582
583 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; }
584
585 int RunParseTests()
586 {
587 int passedCount = 0, failedCount = 0;
588 ParsedGameData gameData;
589 const char *input;
590
591 input ="Level 7\n initialGold 100\nLevel 2 initialGold 200";
592 gameData = (ParsedGameData) {0};
593 EXPECT(ParseGameData(&gameData, input) == 1, "Failed to parse level section");
594 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2");
595 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100");
596 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200");
597
598 input ="Level 392\n";
599 gameData = (ParsedGameData) {0};
600 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
601 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error");
602 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1");
603
604 input ="Level 3\n initialGold -34";
605 gameData = (ParsedGameData) {0};
606 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
607 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error");
608 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1");
609
610 input ="Level 3\n initialGold 2\n initialGold 3";
611 gameData = (ParsedGameData) {0};
612 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
613 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1");
614
615 input ="Level 3";
616 gameData = (ParsedGameData) {0};
617 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
618
619 input ="Level 7\n initialGold 100\nLevel 7 initialGold 200";
620 gameData = (ParsedGameData) {0};
621 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level section");
622 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1");
623
624 input =
625 "Level 7\n initialGold 100\n"
626 "Wave\n"
627 "count 1 wave 2\n"
628 "interval 0.5\n"
629 "delay 1.0\n"
630 "spawnPosition -3 4\n"
631 "enemyType: ENEMY_TYPE_SHIELD"
632 ;
633 gameData = (ParsedGameData) {0};
634 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing level/wave section");
635 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1");
636 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2");
637 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5");
638 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0");
639 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3");
640 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4");
641 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD");
642
643 // for every entry in the wave section, we want to verify that if that value is
644 // missing, the parser will produce an error. We can do that by commenting out each
645 // line individually in a loop - just replacing the two leading spaces with two dashes
646 const char *testString =
647 "Level 7 initialGold 100\n"
648 "Wave\n"
649 " count 1\n"
650 " wave 2\n"
651 " interval 0.5\n"
652 " delay 1.0\n"
653 " spawnPosition 3 -4\n"
654 " enemyType: ENEMY_TYPE_SHIELD";
655 for (int i = 0; testString[i]; i++)
656 {
657 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ')
658 {
659 char copy[1024];
660 strcpy(copy, testString);
661 // commentify!
662 copy[i + 1] = '-';
663 copy[i + 2] = '-';
664 gameData = (ParsedGameData) {0};
665 EXPECT(ParseGameData(&gameData, copy) == 0, "Expected to fail parsing level/wave section");
666 }
667 }
668
669 // test wave section missing data / incorrect data
670
671 input =
672 "Level 7\n initialGold 100\n"
673 "Wave\n"
674 "count 1 wave 2\n"
675 "interval 0.5\n"
676 "delay 1.0\n"
677 "spawnPosition -3\n" // missing y
678 "enemyType: ENEMY_TYPE_SHIELD"
679 ;
680 gameData = (ParsedGameData) {0};
681 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level/wave section");
682 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7");
683
684 input =
685 "Level 7\n initialGold 100\n"
686 "Wave\n"
687 "count 1.0 wave 2\n"
688 "interval 0.5\n"
689 "delay 1.0\n"
690 "spawnPosition -3\n" // missing y
691 "enemyType: ENEMY_TYPE_SHIELD"
692 ;
693 gameData = (ParsedGameData) {0};
694 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing level/wave section");
695 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3");
696
697 // enemy class config parsing tests
698 input =
699 "EnemyClass ENEMY_TYPE_MINION\n"
700 " health: 10.0\n"
701 " speed: 0.6\n"
702 " radius: 0.25\n"
703 " maxAcceleration: 1.0\n"
704 " explosionDamage: 1.0\n"
705 " requiredContactTime: 0.5\n"
706 " explosionRange: 1.0\n"
707 " explosionPushbackPower: 0.25\n"
708 " goldValue: 1\n"
709 ;
710 gameData = (ParsedGameData) {0};
711 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing enemy class section");
712 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0");
713 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6");
714 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25");
715 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0");
716 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0");
717 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5");
718 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0");
719 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25");
720 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1");
721
722 testString =
723 "EnemyClass ENEMY_TYPE_MINION\n"
724 " health: 10.0\n"
725 " speed: 0.6\n"
726 " radius: 0.25\n"
727 " maxAcceleration: 1.0\n"
728 " explosionDamage: 1.0\n"
729 " requiredContactTime: 0.5\n"
730 " explosionRange: 1.0\n"
731 " explosionPushbackPower: 0.25\n"
732 " goldValue: 1\n";
733 for (int i = 0; testString[i]; i++)
734 {
735 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ')
736 {
737 char copy[1024];
738 strcpy(copy, testString);
739 // commentify!
740 copy[i + 1] = '-';
741 copy[i + 2] = '-';
742 gameData = (ParsedGameData) {0};
743 EXPECT(ParseGameData(&gameData, copy) == 0, "Expected to fail parsing EnemyClass section");
744 }
745 }
746
747 input =
748 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
749 " cooldown: 0.5\n"
750 " maxUpgradeCooldown: 0.25\n"
751 " range: 3\n"
752 " maxUpgradeRange: 5\n"
753 " projectileSpeed: 4.0\n"
754 " cost: 5\n"
755 " maxHealth: 10\n"
756 " projectileType: PROJECTILE_TYPE_ARROW\n"
757 " damage: 0.5\n"
758 " maxUpgradeDamage: 1.5\n"
759 " areaDamageRadius: 0\n"
760 " pushbackPowerDistance: 0\n"
761 ;
762 gameData = (ParsedGameData) {0};
763 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing tower type section");
764 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5");
765 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeCooldown == 0.25f, "Expected maxUpgradeCooldown to be 0.25");
766 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].range == 3.0f, "Expected range to be 3.0");
767 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeRange == 5.0f, "Expected maxUpgradeRange to be 5.0");
768 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileSpeed == 4.0f, "Expected projectileSpeed to be 4.0");
769 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 5, "Expected cost to be 5");
770 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
771 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileType == PROJECTILE_TYPE_ARROW, "Expected projectileType to be PROJECTILE_TYPE_ARROW");
772 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.damage == 0.5f, "Expected damage to be 0.5");
773 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.maxUpgradeDamage == 1.5f, "Expected maxUpgradeDamage to be 1.5");
774 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.areaDamageRadius == 0.0f, "Expected areaDamageRadius to be 0.0");
775 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.pushbackPowerDistance == 0.0f, "Expected pushbackPowerDistance to be 0.0");
776
777 input =
778 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
779 " maxHealth: 10\n"
780 " cooldown: 0.5\n"
781 ;
782 gameData = (ParsedGameData) {0};
783 EXPECT(ParseGameData(&gameData, input) == 1, "Expected to succeed parsing tower type section");
784 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5");
785 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
786 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0");
787
788
789 input =
790 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
791 " cooldown: 0.5\n"
792 ;
793 gameData = (ParsedGameData) {0};
794 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing tower type section");
795 EXPECT(TextFindIndex(gameData.parseError, "maxHealth not initialized") >= 0, "Expected to find maxHealth not initialized");
796
797 input =
798 "TowerTypeConfig TOWER_TYPE_ARCHER\n"
799 " maxHealth: 10\n"
800 " foobar: 0.5\n"
801 ;
802 gameData = (ParsedGameData) {0};
803 EXPECT(ParseGameData(&gameData, input) == 0, "Expected to fail parsing tower type section");
804 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10");
805 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0");
806
807 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount);
808
809 return failedCount;
810 }
1 var editButton = document.createElement('button');
2 editButton.innerHTML = 'Edit level data';
3 Module.canvas.insertAdjacentElement('afterend', editButton);
4
5 let logList = [];
6 let updateLogView = function () { };
7 Module.LogMessage = function (logLevel, message) {
8 logList.push([logLevel, message]);
9 if (console && console.log)
10 console.log("LogMessage: ", message);
11 updateLogView();
12 }
13
14 editButton.onclick = function () {
15 editButton.style.display = 'none';
16 let container = document.createElement('div');
17 container.innerHTML = `
18 <div style='border: 1px solid #000; margin:10px; box-shadow:0 3px 10px #0004'>
19 <div style='display: flex; align-items: flex-start; height: 400px;'>
20 <div class='line-numbers' style='padding: 10px 0px; overflow-y: auto; height: 380px;
21 scrollbar-width: none; -ms-overflow-style: none; white-space: pre;
22 background:rgb(44, 45, 48); color:#888; text-align: right; user-select: none; font-family: monospace;'></div>
23 <textarea class='textarea' rows='20' cols='80' spellcheck='false' style='
24 width: 100%; height: 380px; padding: 10px; margin:0; border: none; outline: none; resize: none;
25 overflow-y: auto; font-family: monospace;'></textarea>
26 <div style='width: 100px; background:#353030; height:390px; padding:5px'>
27 <button style='width:100%'>Load data</button>
28 </div>
29 </div>
30 <div class='logs' style='height: 100px; overflow-y: auto; background:#333330; color:#ddd; padding: 10px;
31 font-family:monospace; border-top:1px solid #000'></div>
32 </div>
33 `
34 Module.canvas.insertAdjacentElement('afterend', container);
35
36
37 const codeArea = container.querySelector('.textarea');
38 const lineNumbers = container.querySelector('.line-numbers');
39 const loadButton = container.querySelector('button');
40 const logs = container.querySelector('.logs');
41 updateLogView = function () {
42 logs.innerHTML = logList.map(([logLevel, message]) => {
43 return `<div style="color: ${logLevel > 3 ? '#c60' : '#bbb'}">${message}</div>`;
44 }).join('');
45 logs.scrollTop = logs.scrollHeight;
46 errorLines = {};
47 for (var i = logList.length - 1; i >= 0; i--) {
48 var [logLevel, message] = logList[i];
49 if (logLevel > 3) {
50 var match = message.match(/line (\d+):/);
51 if (match) {
52 errorLines[parseInt(match[1])] = message;
53 break
54 }
55 }
56 break
57 }
58 updateLineNumbers();
59 }
60 updateLogView();
61 var errorLines = {};
62 function updateLineNumbers() {
63 const lines = codeArea.value.split('\n').length;
64 lineNumbers.innerHTML = Array.from({ length: lines },
65 (_, i) => {
66 let error = errorLines[i + 1];
67 let num = (i + 1) + ' ';
68 if (num < 10) num = ' ' + num;
69 if (num < 100) num = ' ' + num;
70 if (error) {
71 return `<div style="background:#833;color:#ec8;text-align:right" title="${error}"> ${num} </div>`;
72 }
73 return `<div style="text-align:right"> ${num} </div>`;
74 }
75 ).join('');
76 }
77 function syncScroll() {
78 lineNumbers.scrollTop = codeArea.scrollTop;
79 }
80
81 codeArea.addEventListener('input', updateLineNumbers);
82 codeArea.addEventListener('scroll', syncScroll);
83 lineNumbers.addEventListener('scroll', syncScroll);
84
85 loadButton.onclick = function () {
86 var levelData = codeArea.value;
87 FS.writeFile('data/level.txt', levelData);
88 Module._RequestReload();
89 }
90
91 codeArea.value = FS.readFile('data/level.txt', { encoding: 'utf8' });
92
93 updateLineNumbers();
94
95 // Function to highlight lines with errors
96 function highlightErrorLines(lines) {
97 const codeLines = codeArea.value.split('\n');
98 const highlightedCode = codeLines.map((line, index) => {
99 return lines.includes(index + 1) ? `<span class="highlight">${line}</span>` : line;
100 }).join('\n');
101 codeArea.innerHTML = highlightedCode;
102 }
103
104 // Example usage: Highlight lines 2 and 4
105 // highlightErrorLines([2, 4]);
106
107 }
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 LEVEL_STATE_RELOAD,
132 } LevelState;
133
134 typedef struct EnemyWave {
135 uint8_t enemyType;
136 uint8_t wave;
137 uint16_t count;
138 float interval;
139 float delay;
140 Vector2 spawnPosition;
141
142 uint16_t spawned;
143 float timeToSpawnNext;
144 } EnemyWave;
145
146 #define ENEMY_MAX_WAVE_COUNT 10
147
148 typedef enum PlacementPhase
149 {
150 PLACEMENT_PHASE_STARTING,
151 PLACEMENT_PHASE_MOVING,
152 PLACEMENT_PHASE_PLACING,
153 } PlacementPhase;
154
155 typedef struct Level
156 {
157 int seed;
158 LevelState state;
159 LevelState nextState;
160 Camera3D camera;
161 int placementMode;
162 PlacementPhase placementPhase;
163 float placementTimer;
164
165 int16_t placementX;
166 int16_t placementY;
167 int8_t placementContextMenuStatus;
168 int8_t placementContextMenuType;
169
170 Vector2 placementTransitionPosition;
171 PhysicsPoint placementTowerSpring;
172
173 int initialGold;
174 int playerGold;
175
176 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
177 int currentWave;
178 float waveEndTimer;
179 } Level;
180
181 typedef struct DeltaSrc
182 {
183 char x, y;
184 } DeltaSrc;
185
186 typedef struct PathfindingMap
187 {
188 int width, height;
189 float scale;
190 float *distances;
191 long *towerIndex;
192 DeltaSrc *deltaSrc;
193 float maxDistance;
194 Matrix toMapSpace;
195 Matrix toWorldSpace;
196 } PathfindingMap;
197
198 // when we execute the pathfinding algorithm, we need to store the active nodes
199 // in a queue. Each node has a position, a distance from the start, and the
200 // position of the node that we came from.
201 typedef struct PathfindingNode
202 {
203 int16_t x, y, fromX, fromY;
204 float distance;
205 } PathfindingNode;
206
207 typedef struct EnemyId
208 {
209 uint16_t index;
210 uint16_t generation;
211 } EnemyId;
212
213 typedef struct EnemyClassConfig
214 {
215 float speed;
216 float health;
217 float shieldHealth;
218 float shieldDamageAbsorption;
219 float radius;
220 float maxAcceleration;
221 float requiredContactTime;
222 float explosionDamage;
223 float explosionRange;
224 float explosionPushbackPower;
225 int goldValue;
226 } EnemyClassConfig;
227
228 typedef struct Enemy
229 {
230 int16_t currentX, currentY;
231 int16_t nextX, nextY;
232 Vector2 simPosition;
233 Vector2 simVelocity;
234 uint16_t generation;
235 float walkedDistance;
236 float startMovingTime;
237 float damage, futureDamage;
238 float shieldDamage;
239 float contactTime;
240 uint8_t enemyType;
241 uint8_t movePathCount;
242 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
243 } Enemy;
244
245 // a unit that uses sprites to be drawn
246 #define SPRITE_UNIT_ANIMATION_COUNT 6
247 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
248 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
249 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
250
251 typedef struct SpriteAnimation
252 {
253 Rectangle srcRect;
254 Vector2 offset;
255 uint8_t animationId;
256 uint8_t frameCount;
257 uint8_t frameWidth;
258 float frameDuration;
259 } SpriteAnimation;
260
261 typedef struct SpriteUnit
262 {
263 float scale;
264 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
265 } SpriteUnit;
266
267 #define PROJECTILE_MAX_COUNT 1200
268 #define PROJECTILE_TYPE_NONE 0
269 #define PROJECTILE_TYPE_ARROW 1
270 #define PROJECTILE_TYPE_CATAPULT 2
271 #define PROJECTILE_TYPE_BALLISTA 3
272
273 typedef struct Projectile
274 {
275 uint8_t projectileType;
276 float shootTime;
277 float arrivalTime;
278 float distance;
279 Vector3 position;
280 Vector3 target;
281 Vector3 directionNormal;
282 EnemyId targetEnemy;
283 HitEffectConfig hitEffectConfig;
284 } Projectile;
285
286 typedef struct ParsedGameData
287 {
288 const char *parseError;
289 Level levels[32];
290 int lastLevelIndex;
291 TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
292 EnemyClassConfig enemyClasses[8];
293 } ParsedGameData;
294
295 //# Function declarations
296 int ParseGameData(ParsedGameData *gameData, const char *input);
297 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
298 int EnemyAddDamageRange(Vector2 position, float range, float damage);
299 int EnemyAddDamage(Enemy *enemy, float damage);
300
301 //# Enemy functions
302 void EnemyInit();
303 void EnemyDraw();
304 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
305 void EnemyUpdate();
306 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
307 float EnemyGetMaxHealth(Enemy *enemy);
308 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
309 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
310 EnemyId EnemyGetId(Enemy *enemy);
311 Enemy *EnemyTryResolve(EnemyId enemyId);
312 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
313 int EnemyAddDamage(Enemy *enemy, float damage);
314 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
315 int EnemyCount();
316 void EnemyDrawHealthbars(Camera3D camera);
317
318 //# Tower functions
319 const char *TowerTypeGetName(uint8_t towerType);
320 int TowerTypeGetCosts(uint8_t towerType);
321 void TowerTypeSetData(uint8_t towerType, TowerTypeConfig *data);
322 void TowerLoadAssets();
323 void TowerInit();
324 void TowerUpdate();
325 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget);
326 void TowerDrawAll();
327 void TowerDrawAllHealthBars(Camera3D camera);
328 void TowerDrawModel(Tower *tower);
329 void TowerDrawRange(Tower *tower, float alpha);
330 Tower *TowerGetByIndex(int index);
331 Tower *TowerGetByType(uint8_t towerType);
332 Tower *TowerGetAt(int16_t x, int16_t y);
333 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
334 float TowerGetMaxHealth(Tower *tower);
335 float TowerGetRange(Tower *tower);
336
337 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
338
339 //# Particles
340 void ParticleInit();
341 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
342 void ParticleUpdate();
343 void ParticleDraw();
344
345 //# Projectiles
346 void ProjectileInit();
347 void ProjectileDraw();
348 void ProjectileUpdate();
349 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
350
351 //# Pathfinding map
352 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
353 float PathFindingGetDistance(int mapX, int mapY);
354 Vector2 PathFindingGetGradient(Vector3 world);
355 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
356 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
357 void PathFindingMapDraw();
358
359 //# UI
360 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth);
361
362 //# Level
363 void DrawLevelGround(Level *level);
364 void DrawEnemyPath(Level *level, Color arrowColor);
365
366 //# variables
367 extern Level *currentLevel;
368 extern Enemy enemies[ENEMY_MAX_COUNT];
369 extern int enemyCount;
370 extern EnemyClassConfig enemyClassConfigs[];
371
372 extern GUIState guiState;
373 extern GameTime gameTime;
374 extern Tower towers[TOWER_MAX_COUNT];
375 extern int towerCount;
376
377 extern Texture2D palette, spriteSheet;
378
379 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .name = "Castle",
7 .maxHealth = 10,
8 },
9 [TOWER_TYPE_ARCHER] = {
10 .name = "Archer",
11 .cooldown = 0.5f,
12 .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 TowerLoadAssets()
129 {
130 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
131 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
132 }
133
134 void TowerInit()
135 {
136 for (int i = 0; i < TOWER_MAX_COUNT; i++)
137 {
138 towers[i] = (Tower){0};
139 }
140 towerCount = 0;
141
142 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
143 {
144 if (towerModels[i].materials)
145 {
146 // assign the palette texture to the material of the model (0 is not used afaik)
147 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
148 }
149 }
150 }
151
152 static float TowerGetCooldown(Tower *tower)
153 {
154 float cooldown = towerTypeConfigs[tower->towerType].cooldown;
155 float maxUpgradeCooldown = towerTypeConfigs[tower->towerType].maxUpgradeCooldown;
156 if (tower->upgradeState.speed > 0)
157 {
158 cooldown = Lerp(cooldown, maxUpgradeCooldown, tower->upgradeState.speed / (float)TOWER_MAX_STAGE);
159 }
160 return cooldown;
161 }
162
163 static void TowerGunUpdate(Tower *tower)
164 {
165 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
166 if (tower->cooldown <= 0.0f)
167 {
168 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, TowerGetRange(tower));
169 if (enemy)
170 {
171 tower->cooldown = TowerGetCooldown(tower);
172 // shoot the enemy; determine future position of the enemy
173 float bulletSpeed = config.projectileSpeed;
174 Vector2 velocity = enemy->simVelocity;
175 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
176 Vector2 towerPosition = {tower->x, tower->y};
177 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
178 for (int i = 0; i < 8; i++) {
179 velocity = enemy->simVelocity;
180 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
181 float distance = Vector2Distance(towerPosition, futurePosition);
182 float eta2 = distance / bulletSpeed;
183 if (fabs(eta - eta2) < 0.01f) {
184 break;
185 }
186 eta = (eta2 + eta) * 0.5f;
187 }
188
189 HitEffectConfig hitEffect = config.hitEffect;
190 // apply damage upgrade to hit effect
191 if (tower->upgradeState.damage > 0)
192 {
193 hitEffect.damage = Lerp(hitEffect.damage, hitEffect.maxUpgradeDamage, tower->upgradeState.damage / (float)TOWER_MAX_STAGE);
194 }
195
196 ProjectileTryAdd(config.projectileType, enemy,
197 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
198 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
199 bulletSpeed, hitEffect);
200 enemy->futureDamage += hitEffect.damage;
201 tower->lastTargetPosition = futurePosition;
202 }
203 }
204 else
205 {
206 tower->cooldown -= gameTime.deltaTime;
207 }
208 }
209
210 void TowerTypeSetData(uint8_t towerType, TowerTypeConfig *data)
211 {
212 towerTypeConfigs[towerType] = *data;
213 }
214
215 Tower *TowerGetAt(int16_t x, int16_t y)
216 {
217 for (int i = 0; i < towerCount; i++)
218 {
219 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
220 {
221 return &towers[i];
222 }
223 }
224 return 0;
225 }
226
227 Tower *TowerGetByIndex(int index)
228 {
229 if (index < 0 || index >= towerCount)
230 {
231 return 0;
232 }
233 return &towers[index];
234 }
235
236 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
237 {
238 if (towerCount >= TOWER_MAX_COUNT)
239 {
240 return 0;
241 }
242
243 Tower *tower = TowerGetAt(x, y);
244 if (tower)
245 {
246 return 0;
247 }
248
249 tower = &towers[towerCount++];
250 *tower = (Tower){
251 .x = x,
252 .y = y,
253 .towerType = towerType,
254 .cooldown = 0.0f,
255 .damage = 0.0f,
256 };
257 return tower;
258 }
259
260 Tower *TowerGetByType(uint8_t towerType)
261 {
262 for (int i = 0; i < towerCount; i++)
263 {
264 if (towers[i].towerType == towerType)
265 {
266 return &towers[i];
267 }
268 }
269 return 0;
270 }
271
272 const char *TowerTypeGetName(uint8_t towerType)
273 {
274 return towerTypeConfigs[towerType].name;
275 }
276
277 int TowerTypeGetCosts(uint8_t towerType)
278 {
279 return towerTypeConfigs[towerType].cost;
280 }
281
282 float TowerGetMaxHealth(Tower *tower)
283 {
284 return towerTypeConfigs[tower->towerType].maxHealth;
285 }
286
287 float TowerGetRange(Tower *tower)
288 {
289 float range = towerTypeConfigs[tower->towerType].range;
290 float maxUpgradeRange = towerTypeConfigs[tower->towerType].maxUpgradeRange;
291 if (tower->upgradeState.range > 0)
292 {
293 range = Lerp(range, maxUpgradeRange, tower->upgradeState.range / (float)TOWER_MAX_STAGE);
294 }
295 return range;
296 }
297
298 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget)
299 {
300 // animate fade in and fade out of range drawing using framerate independent lerp
301 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime);
302 for (int i = 0; i < TOWER_MAX_COUNT; i++)
303 {
304 Tower *fadingTower = TowerGetByIndex(i);
305 if (!fadingTower)
306 {
307 break;
308 }
309 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget;
310 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp);
311 }
312 }
313
314 void TowerDrawRange(Tower *tower, float alpha)
315 {
316 Color ringColor = (Color){255, 200, 100, 255};
317 const int rings = 4;
318 const float radiusOffset = 0.5f;
319 const float animationSpeed = 2.0f;
320 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
321 float radius = TowerGetRange(tower);
322 // base circle
323 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, radius, (Vector3){1, 0, 0}, 90,
324 Fade(ringColor, alpha));
325
326 for (int i = 1; i < rings; i++)
327 {
328 float t = ((float)i + animation) / (float)rings;
329 float r = Lerp(radius, radius - radiusOffset, t * t);
330 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
331 if (i == 1)
332 {
333 // fade out the outermost ring
334 a = animation;
335 }
336 a *= alpha;
337
338 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, r, (Vector3){1, 0, 0}, 90,
339 Fade(ringColor, a));
340 }
341 }
342
343 void TowerDrawModel(Tower *tower)
344 {
345 if (tower->towerType == TOWER_TYPE_NONE)
346 {
347 return;
348 }
349
350 if (tower->drawRangeAlpha > 2.0f/256.0f)
351 {
352 TowerDrawRange(tower, tower->drawRangeAlpha);
353 }
354
355 switch (tower->towerType)
356 {
357 case TOWER_TYPE_ARCHER:
358 {
359 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower->x, 0.0f, tower->y}, currentLevel->camera);
360 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower->lastTargetPosition.x, 0.0f, tower->lastTargetPosition.y}, currentLevel->camera);
361 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
362 DrawSpriteUnit(archerUnit, (Vector3){tower->x, 1.0f, tower->y}, 0, screenPosTarget.x > screenPosTower.x,
363 tower->cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
364 }
365 break;
366 case TOWER_TYPE_BALLISTA:
367 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, BROWN);
368 break;
369 case TOWER_TYPE_CATAPULT:
370 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
371 break;
372 default:
373 if (towerModels[tower->towerType].materials)
374 {
375 DrawModel(towerModels[tower->towerType], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
376 } else {
377 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
378 }
379 break;
380 }
381 }
382
383 void TowerDrawAll()
384 {
385 for (int i = 0; i < towerCount; i++)
386 {
387 TowerDrawModel(&towers[i]);
388 }
389 }
390
391 void TowerUpdate()
392 {
393 for (int i = 0; i < towerCount; i++)
394 {
395 Tower *tower = &towers[i];
396 switch (tower->towerType)
397 {
398 case TOWER_TYPE_CATAPULT:
399 case TOWER_TYPE_BALLISTA:
400 case TOWER_TYPE_ARCHER:
401 TowerGunUpdate(tower);
402 break;
403 }
404 }
405 }
406
407 void TowerDrawAllHealthBars(Camera3D camera)
408 {
409 for (int i = 0; i < towerCount; i++)
410 {
411 Tower *tower = &towers[i];
412 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
413 {
414 continue;
415 }
416
417 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
418 float maxHealth = TowerGetMaxHealth(tower);
419 float health = maxHealth - tower->damage;
420 float healthRatio = health / maxHealth;
421
422 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f);
423 }
424 }
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
After a few changes in the html-edit.js file, the web editor looks fairly good. The logging panel shows all the logs coming from the game, and when there is an error in the configuration file, the line number gutter will highlight the line with the error.
So technically, we can now create a level with all the waves and enemies we want without recompiling the game. What's next?
Next steps
The goal is still "balancing the game". For that purpose, I want to extend our config file with some more options: Adding a test section that allows setting up a test scenario - so specifying which buildings are built and upgraded between the waves. Simulating each wave takes only a few milliseconds, so the idea here is that we can specify a number range for certain parameters and let the game simulate all the combinations, logging each outcome in a way so we can visualize the results in the browser.
So in the next part, we will add this test section and implement the simulation part.