Source code for mini_arcade_core.scenes.systems.builtins.spawn
"""
Reusable spawn and wave progression systems.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Generic, Iterable, Optional, TypeVar, Union
from mini_arcade_core.engine.entities import BaseEntity
from mini_arcade_core.scenes.systems.phases import SystemPhase
# pylint: disable=invalid-name
TCtx = TypeVar("TCtx")
SpawnResult = Optional[Union[BaseEntity, Iterable[BaseEntity]]]
# pylint: enable=invalid-name
def _default_enabled_when(_ctx: object) -> bool:
return True
def _normalize_spawned(spawned: SpawnResult) -> tuple[BaseEntity, ...]:
if spawned is None:
return ()
if isinstance(spawned, BaseEntity):
return (spawned,)
return tuple(entity for entity in spawned if entity is not None)
[docs]
@dataclass(frozen=True)
class SpawnBinding(Generic[TCtx]):
"""
Declarative spawn rule for one spawn source.
"""
should_spawn: Callable[[TCtx], bool]
spawn: Callable[[TCtx], SpawnResult]
on_spawned: Callable[[TCtx, tuple[BaseEntity, ...]], None] | None = None
insert_into_world: bool = True
[docs]
@dataclass
class SpawnSystem(Generic[TCtx]):
"""
Execute reusable spawn rules and insert spawned entities into the world.
"""
name: str = "common_spawn"
phase: int = SystemPhase.SIMULATION
order: int = 25
enabled_when: Callable[[TCtx], bool] = _default_enabled_when
bindings: tuple[SpawnBinding[TCtx], ...] = ()
[docs]
def step(self, ctx: TCtx) -> None:
"""
Execute the spawn rules for each binding.
:param ctx: The context object passed to the system, typically containing
references to the world, scene, and other relevant state.
:type ctx: TCtx
"""
if not self.enabled_when(ctx):
return
for binding in self.bindings:
if not binding.should_spawn(ctx):
continue
spawned = _normalize_spawned(binding.spawn(ctx))
if not spawned:
continue
if binding.insert_into_world:
ctx.world.entities.extend(spawned)
if binding.on_spawned is not None:
binding.on_spawned(ctx, spawned)
[docs]
@dataclass(frozen=True)
class WaveProgressionBinding(Generic[TCtx]):
"""
Declarative wave/lap/round progression rule.
"""
is_complete: Callable[[TCtx], bool]
can_progress: Callable[[TCtx], bool] = _default_enabled_when
advance: Callable[[TCtx], None] | None = None
spawn_next: Callable[[TCtx], SpawnResult] | None = None
on_spawned: Callable[[TCtx, tuple[BaseEntity, ...]], None] | None = None
insert_into_world: bool = True
[docs]
@dataclass
class WaveProgressionSystem(Generic[TCtx]):
"""
Advance wave state and optionally spawn the next batch when complete.
"""
name: str = "common_wave_progression"
phase: int = SystemPhase.SIMULATION
order: int = 80
enabled_when: Callable[[TCtx], bool] = _default_enabled_when
bindings: tuple[WaveProgressionBinding[TCtx], ...] = ()
[docs]
def step(self, ctx: TCtx) -> None:
"""
For each binding, if can_progress and is_complete are both true,
call advance and spawn_next (if provided).
:param ctx: The context object passed to the system, typically containing
references to the world, scene, and other relevant state.
:type ctx: TCtx
"""
if not self.enabled_when(ctx):
return
for binding in self.bindings:
if not binding.can_progress(ctx):
continue
if not binding.is_complete(ctx):
continue
if binding.advance is not None:
binding.advance(ctx)
if binding.spawn_next is None:
continue
spawned = _normalize_spawned(binding.spawn_next(ctx))
if not spawned:
continue
if binding.insert_into_world:
ctx.world.entities.extend(spawned)
if binding.on_spawned is not None:
binding.on_spawned(ctx, spawned)
__all__ = [
"SpawnBinding",
"SpawnSystem",
"WaveProgressionBinding",
"WaveProgressionSystem",
]