SOULGATE/server/waves.py
2026-05-04 03:42:47 +02:00

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