Source code for mini_arcade_core.engine.scenes.scene_manager
"""
Module providing runtime adapters for window and scene management.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, List
from mini_arcade_core.engine.scenes.models import (
SceneEntry,
ScenePolicy,
StackItem,
)
from mini_arcade_core.runtime.context import RuntimeContext
from mini_arcade_core.scenes.registry import SceneRegistry
from mini_arcade_core.utils import logger
if TYPE_CHECKING:
from mini_arcade_core.engine.game import Engine
from mini_arcade_core.scenes.sim_scene import SimScene
[docs]
class SceneAdapter:
"""
Manages multiple scenes (not implemented).
"""
def __init__(self, registry: SceneRegistry, game: Engine):
self._registry = registry
self._stack: List[StackItem] = []
self._game = game
@property
def current_scene(self) -> SimScene | None:
"""
Get the currently active scene.
:return: The active Scene instance, or None if no scene is active.
:rtype: SimScene | None
"""
return self._stack[-1].entry.scene if self._stack else None
@property
def visible_stack(self) -> List[SimScene]:
"""
Return the list of scenes that should be drawn (base + overlays).
We draw from the top-most non-overlay scene upward.
:return: List of visible Scene instances.
:rtype: List[SimScene]
"""
return [e.scene for e in self.visible_entries()]
@property
def listed_scenes(self) -> List[SimScene]:
"""
Return all scenes in the stack.
:return: List of all Scene instances in the stack.
:rtype: List[SimScene]
"""
return [item.entry.scene for item in self._stack]
[docs]
def change(self, scene_id: str):
"""
Change the current scene to the specified scene.
:param scene_id: Identifier of the scene to switch to.
:type scene_id: str
"""
self.clean()
self.push(scene_id, as_overlay=False)
[docs]
def push(
self,
scene_id: str,
*,
as_overlay: bool = False,
policy: ScenePolicy | None = None,
):
"""
Push a new scene onto the scene stack.
:param scene_id: Identifier of the scene to push.
:type scene_id: str
:param as_overlay: Whether to push the scene as an overlay.
:type as_overlay: bool
"""
# default policy based on overlay vs base
if policy is None:
# base scenes: do not block anything by default
policy = ScenePolicy()
runtime_context = RuntimeContext.from_game(self._game)
scene = self._registry.create(
scene_id, runtime_context
) # or whatever your factory call is
if not scene:
logger.warning(f"Failed to create scene with id={scene_id!r}")
return
scene.scene_id = scene_id
scene.on_enter()
entry = SceneEntry(
scene_id=scene_id,
scene=scene,
is_overlay=as_overlay,
policy=policy,
)
self._stack.append(StackItem(entry=entry))
[docs]
def pop(self) -> SimScene | None:
"""
Pop the current scene from the scene stack.
:return: The popped Scene instance, or None if the stack was empty.
:rtype: SimScene | None
"""
if not self._stack:
return
item = self._stack.pop()
item.entry.scene.on_exit()
[docs]
def clean(self):
"""Clean up all scenes from the scene stack."""
while self._stack:
self.pop()
[docs]
def quit(self):
"""Quit the game"""
self._game.quit()
[docs]
def visible_entries(self) -> list[SceneEntry]:
"""
Render from bottom->top unless an opaque entry exists; if so,
render only from that entry up.
:return: List of SceneEntry instances to render.
:rtype: list[SceneEntry]
"""
entries = [i.entry for i in self._stack]
# find highest opaque from top down; render starting there
for idx in range(len(entries) - 1, -1, -1):
if entries[idx].policy.is_opaque:
return entries[idx:]
return entries
[docs]
def update_entries(self) -> list[SceneEntry]:
"""
Tick/update scenes considering blocks_update.
Typical: pause overlay blocks update below it.
:return: List of SceneEntry instances to update.
:rtype: list[SceneEntry]
"""
vis = self.visible_entries()
if not vis:
return []
out = []
for entry in reversed(vis): # top->down
out.append(entry)
if entry.policy.blocks_update:
break
return list(reversed(out)) # bottom->top order
[docs]
def input_entry(self) -> SceneEntry | None:
"""
Who gets input this frame. If top blocks_input, only it receives input.
If not, top still gets input (v1 simple). Later you can allow fall-through.
:return: The SceneEntry that receives input, or None if no scenes are active.
:rtype: SceneEntry | None
"""
vis = self.visible_entries()
if not vis:
return None
# If some scene blocks input, only scenes at/above it can receive.
start_idx = 0
for idx in range(len(vis) - 1, -1, -1):
if vis[idx].policy.blocks_input:
start_idx = idx
break
candidates = vis[start_idx:]
# Pick the top-most candidate that actually receives input.
for entry in reversed(candidates):
if entry.policy.receives_input:
return entry
return None
[docs]
def has_scene(self, scene_id: str) -> bool:
"""
Check if a scene with the given ID exists in the stack.
:param scene_id: Identifier of the scene to check.
:type scene_id: str
:return: True if the scene exists in the stack, False otherwise.
:rtype: bool
"""
return any(item.entry.scene_id == scene_id for item in self._stack)
[docs]
def remove_scene(self, scene_id: str):
"""
Remove a scene with the given ID from the stack.
:param scene_id: Identifier of the scene to remove.
:type scene_id: str
"""
# remove first match from top (overlay is usually near top)
for i in range(len(self._stack) - 1, -1, -1):
if self._stack[i].entry.scene_id == scene_id:
item = self._stack.pop(i)
item.entry.scene.on_exit()
return