Skip to content

Commit b834a0b

Browse files
refactor: move waves configs into app.jsonc (#81)
* feat: make waves config in jsonc * fix: adjust enemy health, spawn counts, and weapon damage for better gameplay balance --------- Co-authored-by: AhmedAmrNabil <ahmedamr24680@gmail.com>
1 parent 73db6c6 commit b834a0b

2 files changed

Lines changed: 170 additions & 32 deletions

File tree

config/app.jsonc

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@
265265
},
266266
{
267267
"type": "Health",
268-
"maxHealth": 60.0
268+
"maxHealth": 100.0
269269
},
270270
{
271271
"type": "Collider",
@@ -335,7 +335,7 @@
335335
},
336336
{
337337
"type": "Health",
338-
"maxHealth": 35.0
338+
"maxHealth": 60.0
339339
},
340340
{
341341
"type": "Collider",
@@ -398,7 +398,7 @@
398398
},
399399
{
400400
"type": "Health",
401-
"maxHealth": 35.0
401+
"maxHealth": 40.0
402402
},
403403
{
404404
"type": "Collider",
@@ -449,6 +449,106 @@
449449
}
450450
]
451451
}
452+
],
453+
"countdownDuration": 3.0,
454+
"waveReward": {
455+
"health": 10.0,
456+
"ammo": 20
457+
},
458+
"waves": [
459+
{
460+
"entries": [{ "enemy": "Monster", "count": 6 }],
461+
"spawnInterval": 2,
462+
"maxAliveEnemies": 6
463+
},
464+
{
465+
"entries": [
466+
{ "enemy": "Monster", "count": 4 },
467+
{ "enemy": "Charger", "count": 3 }
468+
],
469+
"spawnInterval": 2,
470+
"maxAliveEnemies": 7
471+
},
472+
{
473+
"entries": [
474+
{ "enemy": "Monster", "count": 4 },
475+
{ "enemy": "Charger", "count": 2 },
476+
{ "enemy": "Drone", "count": 2 }
477+
],
478+
"spawnInterval": 2,
479+
"maxAliveEnemies": 7,
480+
"healthMul": 1.15,
481+
"damageMul": 1.1,
482+
"speedMul": 1.05
483+
},
484+
{
485+
"entries": [
486+
{ "enemy": "Monster", "count": 5 },
487+
{ "enemy": "Charger", "count": 3 },
488+
{ "enemy": "Drone", "count": 3 }
489+
],
490+
"spawnInterval": 2.0,
491+
"maxAliveEnemies": 8,
492+
"healthMul": 1.2,
493+
"damageMul": 1.2,
494+
"speedMul": 1.1
495+
},
496+
{
497+
"entries": [
498+
{ "enemy": "Charger", "count": 10 }
499+
],
500+
"spawnInterval": 2.0,
501+
"maxAliveEnemies": 10,
502+
"healthMul": 1.3,
503+
"damageMul": 1.3,
504+
"speedMul": 1.15
505+
},
506+
{
507+
"entries": [
508+
{ "enemy": "Monster", "count": 6 },
509+
{ "enemy": "Charger", "count": 5 },
510+
{ "enemy": "Drone", "count": 5 }
511+
],
512+
"spawnInterval": 1.8,
513+
"maxAliveEnemies": 10,
514+
"healthMul": 1.5,
515+
"damageMul": 1.5,
516+
"speedMul": 1.2
517+
},
518+
{
519+
"entries": [
520+
{ "enemy": "Monster", "count": 10 },
521+
{ "enemy": "Charger", "count": 6 },
522+
{ "enemy": "Drone", "count": 6 }
523+
],
524+
"spawnInterval": 1.5,
525+
"maxAliveEnemies": 10,
526+
"healthMul": 1.8,
527+
"damageMul": 1.8,
528+
"speedMul": 1.25
529+
},
530+
{
531+
"entries": [
532+
{ "enemy": "Drone", "count": 15 }
533+
],
534+
"spawnInterval": 1.5,
535+
"maxAliveEnemies": 10,
536+
"healthMul": 2.2,
537+
"damageMul": 1.6,
538+
"speedMul": 1.3
539+
},
540+
{
541+
"entries": [
542+
{ "enemy": "Monster", "count": 10 },
543+
{ "enemy": "Charger", "count": 10 },
544+
{ "enemy": "Drone", "count": 10 }
545+
],
546+
"spawnInterval": 1.2,
547+
"maxAliveEnemies": 10,
548+
"healthMul": 2.5,
549+
"damageMul": 1.8,
550+
"speedMul": 1.35
551+
}
452552
]
453553
}
454554
},
@@ -530,15 +630,15 @@
530630
"type": "Weapon",
531631
"cooldown": 0.1,
532632
"bulletSpeed": 100.0,
533-
"bulletDamage": 20.0,
633+
"bulletDamage": 40.0,
534634
"bulletLifetime": 2.5,
535635
"bulletScale": 0.12,
536636
"fireSound": "gunshot-player",
537637
"reloadSound": "reload",
538638
"reloadTime": 1.0,
539639
"automatic": true,
540640
"magSize": 30,
541-
"maxAmmo": 120,
641+
"maxAmmo": 60,
542642
"muzzleOffset": [1, -0.7, -3],
543643
"trailEnabled": true,
544644
"trailMaxSegments": 8,
@@ -579,15 +679,15 @@
579679
"type": "Weapon",
580680
"cooldown": 0.5,
581681
"bulletSpeed": 520.0,
582-
"bulletDamage": 60.0,
682+
"bulletDamage": 100.0,
583683
"bulletLifetime": 3.0,
584684
"bulletScale": 0.05,
585685
"fireSound": "ssg",
586686
"reloadSound": "reload",
587687
"reloadTime": 1.5,
588688
"automatic": false,
589689
"magSize": 8,
590-
"maxAmmo": 64,
690+
"maxAmmo": 16,
591691
"muzzleOffset": [0.8, -0.5, -3.8],
592692
"trailEnabled": true,
593693
"trailMaxSegments": 8,
@@ -614,15 +714,15 @@
614714
"type": "Weapon",
615715
"cooldown": 0.35,
616716
"bulletSpeed": 120.0,
617-
"bulletDamage": 50.0,
717+
"bulletDamage": 80.0,
618718
"bulletLifetime": 3.0,
619719
"bulletScale": 0.15,
620720
"fireSound": "deagle-shot",
621721
"reloadSound": "reload",
622722
"reloadTime": 1.5,
623723
"automatic": false,
624724
"magSize": 8,
625-
"maxAmmo": 64,
725+
"maxAmmo": 16,
626726
"muzzleOffset": [1, -0.5, -2.3],
627727
"trailEnabled": true,
628728
"trailMaxSegments": 8,

src/game/systems/enemy-spawner.hpp

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ namespace gameplay {
5353
{42.0f, 0.0f, 0.0f}, {-42.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 42.0f}, {0.0f, 0.0f, -42.0f},
5454
};
5555
std::vector<EnemyTemplateConfig> enemies;
56+
float countdownDuration = 4.0f;
5657
};
5758

5859
class EnemySpawner {
@@ -62,6 +63,7 @@ namespace gameplay {
6263
private:
6364
EnemySpawnConfig config;
6465
std::vector<WaveConfig> waves;
66+
WaveReward waveReward;
6567

6668
int currentWave = 0;
6769
int waveDisplayNumber = 1;
@@ -78,25 +80,12 @@ namespace gameplay {
7880

7981
bool waveJustCompleted = false;
8082

81-
// utility to build the wave table
82-
void buildWaveTable() {
83-
int B = 0;
84-
int C = std::min(1, (int)config.enemies.size() - 1);
85-
int F = std::min(2, (int)config.enemies.size() - 1);
86-
87-
waves.clear();
88-
89-
// interval, maxAlive, hpMax, dmgMax, speedMax
90-
waves.push_back({{{B, 4}}, 3.0f, 4, 1.0f, 1.0f, 1.0f});
91-
waves.push_back({{{B, 5}, {C, 1}}, 2.5f, 5, 1.0f, 1.0f, 1.0f});
92-
waves.push_back({{{B, 5}, {C, 2}, {F, 1}}, 2.5f, 6, 1.15f, 1.1f, 1.05f});
93-
waves.push_back({{{B, 5}, {C, 3}, {F, 2}}, 2.0f, 7, 1.3f, 1.2f, 1.1f});
94-
waves.push_back({{{B, 5}, {C, 4}, {F, 3}}, 2.0f, 8, 1.5f, 1.3f, 1.15f});
95-
waves.push_back({{{B, 6}, {C, 4}, {F, 4}}, 1.8f, 9, 1.7f, 1.4f, 1.2f});
96-
waves.push_back({{{B, 6}, {C, 5}, {F, 5}}, 1.5f, 10, 1.9f, 1.5f, 1.25f});
97-
waves.push_back({{{B, 7}, {C, 5}, {F, 6}}, 1.5f, 11, 2.2f, 1.6f, 1.3f});
98-
waves.push_back({{{B, 7}, {C, 6}, {F, 7}}, 1.2f, 12, 2.5f, 1.8f, 1.35f});
99-
waves.push_back({{{B, 8}, {C, 8}, {F, 8}}, 1.0f, 14, 3.0f, 2.0f, 1.4f});
83+
// resolve an enemy name to its index in config.enemies, returns 0 if not found
84+
int resolveEnemyIndex(const std::string& name) const {
85+
for (int i = 0; i < (int)config.enemies.size(); i++) {
86+
if (config.enemies[i].name == name) return i;
87+
}
88+
return 0;
10089
}
10190

10291
int countAliveEnemies(our::World* world) const {
@@ -192,6 +181,8 @@ namespace gameplay {
192181

193182
const nlohmann::json& spawnerConfig = sceneConfig["game"]["enemySpawner"];
194183

184+
config.countdownDuration = spawnerConfig.value("countdownDuration", config.countdownDuration);
185+
195186
if (spawnerConfig.contains("spawnPoints") && spawnerConfig["spawnPoints"].is_array()) {
196187
config.spawnPoints.clear();
197188
for (const nlohmann::json& spawnPoint : spawnerConfig["spawnPoints"])
@@ -218,14 +209,61 @@ namespace gameplay {
218209
} else {
219210
config.enemies = {};
220211
}
212+
213+
// Deserialize waves
214+
if (spawnerConfig.contains("waves") && spawnerConfig["waves"].is_array()) {
215+
waves.clear();
216+
for (const nlohmann::json& waveJson : spawnerConfig["waves"]) {
217+
if (!waveJson.is_object()) continue;
218+
219+
WaveConfig wave;
220+
wave.spawnInterval = waveJson.value("spawnInterval", wave.spawnInterval);
221+
wave.maxAliveEnemies = waveJson.value("maxAliveEnemies", wave.maxAliveEnemies);
222+
wave.healthMul = waveJson.value("healthMul", wave.healthMul);
223+
wave.damageMul = waveJson.value("damageMul", wave.damageMul);
224+
wave.speedMul = waveJson.value("speedMul", wave.speedMul);
225+
226+
if (waveJson.contains("entries") && waveJson["entries"].is_array()) {
227+
for (const nlohmann::json& entryJson : waveJson["entries"]) {
228+
if (!entryJson.is_object()) continue;
229+
230+
WaveEntry entry;
231+
entry.count = entryJson.value("count", 0);
232+
233+
// resolve enemy by name, fall back to index
234+
if (entryJson.contains("enemy") && entryJson["enemy"].is_string()) {
235+
entry.templateIndex = resolveEnemyIndex(entryJson["enemy"].get<std::string>());
236+
} else {
237+
entry.templateIndex = entryJson.value("templateIndex", 0);
238+
}
239+
240+
wave.entries.push_back(entry);
241+
}
242+
}
243+
244+
waves.push_back(wave);
245+
}
246+
}
247+
248+
// Deserialize wave reward
249+
if (spawnerConfig.contains("waveReward") && spawnerConfig["waveReward"].is_object()) {
250+
const nlohmann::json& rewardJson = spawnerConfig["waveReward"];
251+
waveReward.health = rewardJson.value("health", waveReward.health);
252+
waveReward.ammo = rewardJson.value("ammo", waveReward.ammo);
253+
}
221254
}
222255

223256
void initialize(our::World* world) {
224-
buildWaveTable();
257+
// ensure at least one fallback wave if none were deserialized
258+
if (waves.empty()) {
259+
WaveConfig fallback;
260+
fallback.entries = {{0, 4}};
261+
waves.push_back(fallback);
262+
}
225263
currentWave = 0;
226264
waveDisplayNumber = 1;
227265
waveState = WaveState::Countdown;
228-
countdownTimer = 4.0f;
266+
countdownTimer = config.countdownDuration;
229267
nextSpawnPoint = 0;
230268
waveJustCompleted = false;
231269
}
@@ -270,7 +308,7 @@ namespace gameplay {
270308
// go next wave
271309
currentWave++;
272310
waveDisplayNumber++;
273-
countdownTimer = 4.0f; // countdown to next wave
311+
countdownTimer = config.countdownDuration;
274312
waveState = WaveState::Countdown;
275313
}
276314
return waveJustCompleted;
@@ -297,7 +335,7 @@ namespace gameplay {
297335
}
298336

299337
WaveReward getWaveReward() const {
300-
return {10.0f, 20};
338+
return waveReward;
301339
}
302340
};
303341

0 commit comments

Comments
 (0)