# waves.py : machine a etats des vagues (combat / boss / preparation / victory) from dataclasses import dataclass from constants import PREPARATION_DURATION, TICK_DURATION, WAVE_BOSSES @dataclass class SpawnGroup: enemy_type: str # type d'ennemi à spawner : "fracture", "rampant", "colosse", "eclat" count: int # nombre total d'ennemis à spawner dans ce groupe interval: int # ticks entre chaque spawn (ex: 40 ticks = 2s à 20Hz) # 20 Hz = 20 ticks par seconde → 40 ticks = 2 secondes @dataclass class PhaseConfig: """Configuration d'une phase : plusieurs groupes d'ennemis spawnent en parallèle.""" groups: list[SpawnGroup] # chaque groupe spawn indépendamment selon son interval # Données des vagues # WAVE_CONFIGS[i] = liste de PhaseConfig pour la vague i+1 # Chaque vague a plusieurs phases. Dans chaque phase, plusieurs groupes spawnent en parallèle. WAVE_CONFIGS: list[list[PhaseConfig]] = [ # Vague 1 — L'Éveil (5 phases de difficulté croissante) [ PhaseConfig([SpawnGroup("fracture", 4, 40)]), # phase 1 : 4 fractures, 1 toutes les 2s PhaseConfig([SpawnGroup("rampant", 6, 30)]), # phase 2 : 6 rampants, 1 toutes les 1.5s PhaseConfig([SpawnGroup("fracture", 4, 30), SpawnGroup("rampant", 3, 60)]), # phase 3 : les deux en même temps PhaseConfig([SpawnGroup("colosse", 2, 80), SpawnGroup("fracture", 4, 40)]), # phase 4 : colosses + fractures PhaseConfig([SpawnGroup("fracture", 6, 20), SpawnGroup("rampant", 6, 20), SpawnGroup("eclat", 4, 15)]), # phase 5 : chaos total ], # Vague 2 — Le Héraut (3 phases, puis boss Vexaris) [ PhaseConfig([SpawnGroup("fracture", 6, 30), SpawnGroup("rampant", 4, 40)]), PhaseConfig([SpawnGroup("eclat", 8, 15), SpawnGroup("colosse", 2, 80)]), PhaseConfig([SpawnGroup("fracture", 8, 20), SpawnGroup("rampant", 6, 25), SpawnGroup("eclat", 6, 15)]), ], # Vague 3 — Morveth (3 phases encore plus dures, puis boss Morveth) [ PhaseConfig([SpawnGroup("fracture", 10, 15), SpawnGroup("rampant", 8, 20), SpawnGroup("colosse", 3, 60)]), PhaseConfig([SpawnGroup("eclat", 10, 10), SpawnGroup("fracture", 8, 15), SpawnGroup("colosse", 3, 60)]), PhaseConfig([SpawnGroup("fracture", 12, 10), SpawnGroup("rampant", 10, 15), SpawnGroup("colosse", 4, 50), SpawnGroup("eclat", 8, 12)]), ], ] # WaveManager class WaveManager: """ machine a etats : combat -> boss -> preparation -> ... -> victory """ def __init__(self): self._wave_idx = 0 self._phase_idx = 0 self._state = "preparation" self._prep_timer = PREPARATION_DURATION self._initial_prep = True self._phase_spawned = [] self._phase_ticks = [] self._total_to_spawn = 0 self._total_spawned = 0 self._boss_spawned = False self._init_phase() @property def wave_number(self): return self._wave_idx + 1 # affiche 1/2/3 @property def phase_number(self): return self._phase_idx + 1 @property def state(self): return self._state @property def prep_timer_remaining(self): return max(0.0, self._prep_timer) def _current_phase(self): return WAVE_CONFIGS[self._wave_idx][self._phase_idx] def _init_phase(self): phase = self._current_phase() n = len(phase.groups) self._phase_spawned = [0] * n # ticks initialises a l'intervalle pour spawn des le 1er tick self._phase_ticks = [g.interval for g in phase.groups] self._total_to_spawn = sum(g.count for g in phase.groups) self._total_spawned = 0 def enemies_remaining(self): # ennemis restants a spawner dans la phase courante (pas ceux vivants sur la map) return self._total_to_spawn - self._total_spawned def tick(self, spawn_fn, enemy_count): events = [] if self._state == "victory": return events # phase boss : on spawn le boss 1 fois, puis on attend qu'il meure if self._state == "boss": if not self._boss_spawned: spawn_fn(WAVE_BOSSES[self._wave_idx + 1]) self._boss_spawned = True elif enemy_count == 0: next_wave = self._wave_idx + 1 if next_wave < len(WAVE_CONFIGS): self._state = "preparation" self._prep_timer = PREPARATION_DURATION events.append("wave_complete") events.append("preparation_start") else: self._state = "victory" events.append("game_over_victory") return events # phase preparation : timer 20s avant la vague suivante if self._state == "preparation": self._prep_timer -= TICK_DURATION if self._prep_timer <= 0.0: if self._initial_prep: self._initial_prep = False # vague 1 : pas d'increment else: self._wave_idx += 1 self._phase_idx = 0 self._init_phase() self._state = "combat" events.append("wave_start") return events # combat : spawn des groupes en parallele phase = self._current_phase() for i, group in enumerate(phase.groups): if self._phase_spawned[i] >= group.count: continue self._phase_ticks[i] += 1 if self._phase_ticks[i] >= group.interval: spawn_fn(group.enemy_type) self._phase_spawned[i] += 1 self._total_spawned += 1 self._phase_ticks[i] = 0 # fin de phase : tout spawne ET 0 ennemis vivants all_spawned = self._total_spawned >= self._total_to_spawn if all_spawned and enemy_count == 0: next_phase = self._phase_idx + 1 if next_phase < len(WAVE_CONFIGS[self._wave_idx]): self._phase_idx = next_phase self._init_phase() events.append("wave_phase") else: wave_number = self._wave_idx + 1 next_wave = self._wave_idx + 1 if wave_number in WAVE_BOSSES: self._state = "boss" self._boss_spawned = False events.append("boss_incoming") elif next_wave < len(WAVE_CONFIGS): self._state = "preparation" self._prep_timer = PREPARATION_DURATION events.append("wave_complete") events.append("preparation_start") else: # Dernière vague sans boss → victoire self._state = "victory" events.append("game_over_victory") return events