181 lines
6.8 KiB
Python
181 lines
6.8 KiB
Python
# 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
|