Skip to content

Commit 68f5217

Browse files
committed
Add DifficultyManager and scale enemy spawn/difficulty
1 parent 6007bce commit 68f5217

7 files changed

Lines changed: 281 additions & 6 deletions

File tree

Source/Games/SpaceShooter/Enemies/Enemy.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public partial class Enemy : Node2D
6161
[Export] private float _hitShakeStrength = 16.0f;
6262
[Export] private int _hitShakeSteps = 12;
6363
[Export] private int _scoreValue = 100;
64+
private float _baseDepthSpeed;
65+
private int _baseScoreValue;
66+
private int _baseMaxHp;
67+
private int _currentScoreValue;
6468

6569
[Export] private EnemyFlightPattern _flightPattern = EnemyFlightPattern.Forward;
6670
private Vector2 _startPoint;
@@ -95,9 +99,13 @@ public override void _Ready()
9599
if (_healthComponent != null)
96100
{
97101
_healthComponent.HealthChanged += OnHealthChanged;
102+
_baseMaxHp = Math.Max(1, _healthComponent.MaxHp);
98103
}
99104

100105
_particlesComponent ??= GetNodeOrNull<ParticlesComponent>("ParticlesComponent");
106+
_baseDepthSpeed = Mathf.Max(0.01f, _depthSpeed);
107+
_baseScoreValue = Math.Max(0, _scoreValue);
108+
_currentScoreValue = _baseScoreValue;
101109

102110
Scale = Vector2.One * Mathf.Max(0.01f, _minScale);
103111
}
@@ -178,6 +186,37 @@ public void ConfigureSpawn(float startY, EnemySpawnOrigin spawnOrigin, EnemyTarg
178186
Scale = Vector2.One * Mathf.Max(0.01f, initialScale);
179187
}
180188

189+
public void ApplyDifficulty(float speedMultiplier, float healthMultiplier, float scoreMultiplier)
190+
{
191+
if (_baseDepthSpeed <= 0f)
192+
{
193+
_baseDepthSpeed = Mathf.Max(0.01f, _depthSpeed);
194+
}
195+
196+
if (_baseScoreValue <= 0)
197+
{
198+
_baseScoreValue = Math.Max(0, _scoreValue);
199+
}
200+
201+
if (_healthComponent != null && _baseMaxHp <= 0)
202+
{
203+
_baseMaxHp = Math.Max(1, _healthComponent.MaxHp);
204+
}
205+
206+
float safeSpeedMultiplier = Mathf.Clamp(speedMultiplier, 0.5f, 4.0f);
207+
float safeHealthMultiplier = Mathf.Clamp(healthMultiplier, 0.5f, 5.0f);
208+
float safeScoreMultiplier = Mathf.Clamp(scoreMultiplier, 0.5f, 5.0f);
209+
210+
_depthSpeed = Mathf.Max(0.01f, _baseDepthSpeed * safeSpeedMultiplier);
211+
_currentScoreValue = Math.Max(0, Mathf.RoundToInt(_baseScoreValue * safeScoreMultiplier));
212+
213+
if (_healthComponent != null)
214+
{
215+
int scaledMaxHp = Math.Max(1, Mathf.RoundToInt(_baseMaxHp * safeHealthMultiplier));
216+
_healthComponent.MaxHp = scaledMaxHp;
217+
}
218+
}
219+
181220
public void HandleDeath()
182221
{
183222
if (_isDying || !_isActive)
@@ -188,7 +227,7 @@ public void HandleDeath()
188227
_isDying = true;
189228
Node particleParent = GetParent() ?? GetTree()?.CurrentScene;
190229
_particlesComponent?.EmitAt(GlobalPosition, particleParent);
191-
EmitSignal(SignalName.Defeated, Math.Max(0, _scoreValue));
230+
EmitSignal(SignalName.Defeated, Math.Max(0, _currentScoreValue));
192231
RequestRecycle(0);
193232
}
194233

Source/Games/SpaceShooter/EnemySpawner/EnemySpawner.cs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@ public partial class EnemySpawner : Node
1919
private readonly RandomNumberGenerator _rng = new();
2020
private readonly Dictionary<PackedScene, Stack<Enemy>> _enemyPoolByScene = new();
2121
private readonly Dictionary<Enemy, PackedScene> _sceneByEnemy = new();
22+
private readonly HashSet<Enemy> _activeEnemies = new();
2223
private int _enemyNameCounter;
2324
private Timer _spawnTimer;
2425
private bool _isSpawningEnabled = true;
26+
private float _baseSpawnIntervalMin;
27+
private float _baseSpawnIntervalMax;
28+
private float _spawnRateMultiplier = 1f;
29+
private float _spawnCountMultiplier = 1f;
30+
private float _enemySpeedMultiplier = 1f;
31+
private float _enemyHealthMultiplier = 1f;
32+
private float _enemyScoreMultiplier = 1f;
2533

2634
public override void _Ready()
2735
{
@@ -40,6 +48,9 @@ public override void _Ready()
4048
Autostart = false
4149
};
4250

51+
_baseSpawnIntervalMin = Mathf.Max(0.05f, _spawnIntervalMin);
52+
_baseSpawnIntervalMax = Mathf.Max(_baseSpawnIntervalMin, _spawnIntervalMax);
53+
4354
AddChild(_spawnTimer);
4455
_spawnTimer.Timeout += OnSpawnTimerTimeout;
4556

@@ -64,7 +75,12 @@ private void OnSpawnTimerTimeout()
6475
return;
6576
}
6677

67-
SpawnEnemy();
78+
int spawnCount = CalculateSpawnCount();
79+
for (int i = 0; i < spawnCount; i++)
80+
{
81+
SpawnEnemy();
82+
}
83+
6884
ScheduleNextSpawn();
6985
}
7086

@@ -75,13 +91,40 @@ private void ScheduleNextSpawn()
7591
return;
7692
}
7793

78-
float minInterval = Mathf.Max(0.05f, Math.Min(_spawnIntervalMin, _spawnIntervalMax));
79-
float maxInterval = Mathf.Max(minInterval, Math.Max(_spawnIntervalMin, _spawnIntervalMax));
94+
float baseMinInterval = Mathf.Max(0.05f, Math.Min(_baseSpawnIntervalMin, _baseSpawnIntervalMax));
95+
float baseMaxInterval = Mathf.Max(baseMinInterval, Math.Max(_baseSpawnIntervalMin, _baseSpawnIntervalMax));
96+
float safeRateMultiplier = Mathf.Max(0.1f, _spawnRateMultiplier);
97+
float minInterval = Mathf.Max(0.05f, baseMinInterval / safeRateMultiplier);
98+
float maxInterval = Mathf.Max(minInterval, baseMaxInterval / safeRateMultiplier);
8099

81100
_spawnTimer.WaitTime = _rng.RandfRange(minInterval, maxInterval);
82101
_spawnTimer.Start();
83102
}
84103

104+
public void SetDifficultyMultipliers(
105+
float spawnRateMultiplier,
106+
float spawnCountMultiplier,
107+
float enemySpeedMultiplier,
108+
float enemyHealthMultiplier,
109+
float enemyScoreMultiplier)
110+
{
111+
_spawnRateMultiplier = Mathf.Clamp(spawnRateMultiplier, 0.4f, 4.0f);
112+
_spawnCountMultiplier = Mathf.Clamp(spawnCountMultiplier, 1.0f, 4.0f);
113+
_enemySpeedMultiplier = Mathf.Clamp(enemySpeedMultiplier, 0.5f, 4.0f);
114+
_enemyHealthMultiplier = Mathf.Clamp(enemyHealthMultiplier, 0.5f, 5.0f);
115+
_enemyScoreMultiplier = Mathf.Clamp(enemyScoreMultiplier, 0.5f, 5.0f);
116+
117+
foreach (Enemy activeEnemy in _activeEnemies)
118+
{
119+
if (!GodotObject.IsInstanceValid(activeEnemy))
120+
{
121+
continue;
122+
}
123+
124+
activeEnemy.ApplyDifficulty(_enemySpeedMultiplier, _enemyHealthMultiplier, _enemyScoreMultiplier);
125+
}
126+
}
127+
85128
public void SetSpawningEnabled(bool isEnabled)
86129
{
87130
if (_isSpawningEnabled == isEnabled)
@@ -184,6 +227,9 @@ private Enemy AcquireEnemy(PackedScene enemyScene, Node targetParent)
184227
}
185228
}
186229

230+
enemy.ApplyDifficulty(_enemySpeedMultiplier, _enemyHealthMultiplier, _enemyScoreMultiplier);
231+
_activeEnemies.Add(enemy);
232+
187233
return enemy;
188234
}
189235

@@ -212,6 +258,7 @@ private void ReturnEnemyToPool(Enemy enemy)
212258

213259
if (!_sceneByEnemy.TryGetValue(enemy, out PackedScene enemyScene) || enemyScene == null)
214260
{
261+
_activeEnemies.Remove(enemy);
215262
enemy.QueueFree();
216263
return;
217264
}
@@ -222,6 +269,7 @@ private void ReturnEnemyToPool(Enemy enemy)
222269
_enemyPoolByScene[enemyScene] = scenePool;
223270
}
224271

272+
_activeEnemies.Remove(enemy);
225273
enemy.DeactivateForPool();
226274
scenePool.Push(enemy);
227275
}
@@ -299,4 +347,18 @@ private EnemyTargetLane PickTargetLane(EnemySpawnOrigin spawnOrigin)
299347
return (EnemyTargetLane)_rng.RandiRange(0, 2);
300348
}
301349
}
350+
351+
private int CalculateSpawnCount()
352+
{
353+
float safeMultiplier = Mathf.Max(1f, _spawnCountMultiplier);
354+
int guaranteedCount = Math.Max(1, Mathf.FloorToInt(safeMultiplier));
355+
float extraSpawnChance = Mathf.Clamp(safeMultiplier - guaranteedCount, 0f, 1f);
356+
357+
if (_rng.Randf() < extraSpawnChance)
358+
{
359+
guaranteedCount++;
360+
}
361+
362+
return guaranteedCount;
363+
}
302364
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using Godot;
2+
using System;
3+
4+
public partial class DifficultyManager : Node
5+
{
6+
[Export] private EnemySpawner _enemySpawner;
7+
[Export] private float _targetHardTimeSeconds = 240f;
8+
[Export] private float _maxSpawnRateMultiplier = 1.5f;
9+
[Export] private float _maxSpawnCountMultiplier = 1.3f;
10+
[Export] private float _maxEnemySpeedMultiplier = 1.3f;
11+
[Export] private float _maxEnemyHealthMultiplier = 1.4f;
12+
[Export] private float _maxEnemyScoreMultiplier = 1.4f;
13+
14+
private SpaceShooterGameManager _gameManager;
15+
private float _elapsedSeconds;
16+
private bool _isConfigured;
17+
private float _updateAccumulator;
18+
private const float UpdateIntervalSeconds = 0.25f;
19+
20+
public override void _Ready()
21+
{
22+
_gameManager = SpaceShooterGameManager.GetOrNull(this);
23+
if (_gameManager != null)
24+
{
25+
_gameManager.SpaceShooterActiveChanged += OnSpaceShooterActiveChanged;
26+
_gameManager.DefeatStateChanged += OnDefeatStateChanged;
27+
}
28+
29+
_isConfigured = false;
30+
_elapsedSeconds = 0f;
31+
_updateAccumulator = 0f;
32+
}
33+
34+
public override void _ExitTree()
35+
{
36+
if (_gameManager != null)
37+
{
38+
_gameManager.SpaceShooterActiveChanged -= OnSpaceShooterActiveChanged;
39+
_gameManager.DefeatStateChanged -= OnDefeatStateChanged;
40+
}
41+
}
42+
43+
public override void _Process(double delta)
44+
{
45+
if (!CanProgressDifficulty())
46+
{
47+
return;
48+
}
49+
50+
float deltaF = Mathf.Max(0f, (float)delta);
51+
_elapsedSeconds += deltaF;
52+
_updateAccumulator += deltaF;
53+
54+
if (_updateAccumulator < UpdateIntervalSeconds)
55+
{
56+
return;
57+
}
58+
59+
_updateAccumulator = 0f;
60+
ApplyCurrentDifficulty();
61+
}
62+
63+
public void Configure(EnemySpawner enemySpawner)
64+
{
65+
_enemySpawner = enemySpawner;
66+
_isConfigured = true;
67+
ResetDifficultyProgress();
68+
}
69+
70+
private bool CanProgressDifficulty()
71+
{
72+
if (!_isConfigured || _enemySpawner == null)
73+
{
74+
return false;
75+
}
76+
77+
if (_gameManager == null)
78+
{
79+
return true;
80+
}
81+
82+
return _gameManager.IsSpaceShooterActive && !_gameManager.IsDefeated;
83+
}
84+
85+
private void OnSpaceShooterActiveChanged(bool isActive)
86+
{
87+
if (isActive)
88+
{
89+
ResetDifficultyProgress();
90+
}
91+
}
92+
93+
private void OnDefeatStateChanged(bool isDefeated)
94+
{
95+
if (!isDefeated)
96+
{
97+
ResetDifficultyProgress();
98+
}
99+
}
100+
101+
private void ResetDifficultyProgress()
102+
{
103+
_elapsedSeconds = 0f;
104+
_updateAccumulator = 0f;
105+
ApplyCurrentDifficulty();
106+
}
107+
108+
private void ApplyCurrentDifficulty()
109+
{
110+
if (_enemySpawner == null)
111+
{
112+
return;
113+
}
114+
115+
float hardTime = Mathf.Max(60f, _targetHardTimeSeconds);
116+
float progress = Mathf.Clamp(_elapsedSeconds / hardTime, 0f, 1f);
117+
float smoothProgress = Mathf.SmoothStep(0f, 1f, progress);
118+
float lateGamePressure = Mathf.Pow(progress, 1.5f);
119+
float pressure = Mathf.Lerp(smoothProgress, lateGamePressure, 0.4f);
120+
121+
float spawnRateMultiplier = BuildMultiplier(_maxSpawnRateMultiplier, pressure);
122+
float spawnCountMultiplier = BuildMultiplier(_maxSpawnCountMultiplier, pressure);
123+
float enemySpeedMultiplier = BuildMultiplier(_maxEnemySpeedMultiplier, pressure);
124+
float enemyHealthMultiplier = BuildMultiplier(_maxEnemyHealthMultiplier, pressure);
125+
float enemyScoreMultiplier = BuildMultiplier(_maxEnemyScoreMultiplier, pressure);
126+
127+
_enemySpawner.SetDifficultyMultipliers(
128+
spawnRateMultiplier,
129+
spawnCountMultiplier,
130+
enemySpeedMultiplier,
131+
enemyHealthMultiplier,
132+
enemyScoreMultiplier);
133+
}
134+
135+
private static float BuildMultiplier(float maxMultiplier, float pressure)
136+
{
137+
float safeMax = Mathf.Max(1f, maxMultiplier);
138+
float clampedPressure = Mathf.Clamp(pressure, 0f, 1f);
139+
return Mathf.Lerp(1f, safeMax, clampedPressure);
140+
}
141+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://be03i56iby2x4
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[gd_scene format=3 uid="uid://7cjm8tig0i18"]
2+
3+
[ext_resource type="Script" uid="uid://be03i56iby2x4" path="res://Source/Games/SpaceShooter/Managers/DifficultyManager/DifficultyManager.cs" id="1_22j5r"]
4+
5+
[node name="DifficultyManager" type="Node" unique_id=1994103712]
6+
script = ExtResource("1_22j5r")

0 commit comments

Comments
 (0)