from __future__ import annotations # isort: off from kvui import GameManager, MDNavigationItemBase # isort: on from typing import TYPE_CHECKING, Any from kivy._clock import ClockEvent from kivy.clock import Clock from kivy.uix.gridlayout import GridLayout from kivy.uix.image import Image from kivy.uix.layout import Layout from kivymd.uix.recycleview import MDRecycleView from ..game.game import Game from ..game.graphics import Graphic from .custom_views import ( APQuestControlsView, APQuestGameView, APQuestGrid, ConfettiView, TapIfConfettiCannonImage, TapImage, VolumeSliderView, ) from .graphics import PlayerSprite, get_texture from .sounds import SoundManager if TYPE_CHECKING: from .ap_quest_client import APQuestContext class APQuestManager(GameManager): base_title = "APQuest for AP version" ctx: APQuestContext lower_game_grid: GridLayout upper_game_grid: GridLayout game_view: MDRecycleView game_view_tab: MDNavigationItemBase sound_manager: SoundManager bottom_image_grid: list[list[Image]] top_image_grid: list[list[TapImage]] confetti_view: ConfettiView move_event: ClockEvent | None bottom_grid_is_grass: bool def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.sound_manager = SoundManager() self.sound_manager.allow_intro_to_play = not self.ctx.delay_intro_song self.top_image_grid = [] self.bottom_image_grid = [] self.move_event = None self.bottom_grid_is_grass = False def allow_intro_song(self) -> None: self.sound_manager.allow_intro_to_play = True def add_confetti(self, position: tuple[float, float], amount: int) -> None: self.confetti_view.add_confetti(position, amount) def play_jingle(self, audio_filename: str) -> None: self.sound_manager.play_jingle(audio_filename) def switch_to_tab(self, desired_tab: MDNavigationItemBase) -> None: if self.screens.current_tab == desired_tab: return self.screens.current_tab.active = False self.screens.switch_screens(desired_tab) desired_tab.active = True def switch_to_game_tab(self) -> None: self.switch_to_tab(self.game_view_tab) def switch_to_regular_tab(self) -> None: self.switch_to_tab(self.tabs.children[-1]) def game_started(self) -> None: self.switch_to_game_tab() self.sound_manager.game_started = True def render(self, game: Game, player_sprite: PlayerSprite) -> None: self.setup_game_grid_if_not_setup(game) # This calls game.render(), which needs to happen to update the state of math traps self.render_gameboard(game, player_sprite) # Only now can we check whether a math problem is active self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None) self.sound_manager.math_trap_active = game.active_math_problem is not None self.render_item_column(game) def render_gameboard(self, game: Game, player_sprite: PlayerSprite) -> None: rendered_gameboard = game.render() for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False): for graphic, image in zip(gameboard_row, image_row[:11], strict=False): texture = get_texture(graphic, player_sprite) if texture is None: image.opacity = 0 image.texture = None continue image.texture = texture image.opacity = 1 def render_item_column(self, game: Game) -> None: rendered_item_column = game.render_health_and_inventory(vertical=True) for item_graphic, image_row in zip(rendered_item_column, self.top_image_grid, strict=False): image = image_row[-1] image.is_confetti_cannon = item_graphic == Graphic.CONFETTI_CANNON texture = get_texture(item_graphic) if texture is None: image.opacity = 0 image.texture = None continue image.texture = texture image.opacity = 1 def render_background_game_grid(self, size: tuple[int, int], grass: bool) -> None: if grass == self.bottom_grid_is_grass: return for row in range(size[1]): for column in range(size[0]): image = self.bottom_image_grid[row][column] if not grass: image.color = (0.3, 0.3, 0.3) image.texture = None continue boss_room = (row in (0, 1, 2) and (size[1] - column) in (1, 2, 3)) or (row, column) == (3, size[1] - 2) if boss_room: image.color = (0.45, 0.35, 0.1) image.texture = None continue image.texture = get_texture("Grass") image.color = (1.0, 1.0, 1.0) self.bottom_grid_is_grass = grass def setup_game_grid_if_not_setup(self, game: Game) -> None: if self.upper_game_grid.children: return self.top_image_grid = [] self.bottom_image_grid = [] size = game.gameboard.size for row in range(size[1]): self.top_image_grid.append([]) self.bottom_image_grid.append([]) for column in range(size[0]): bottom_image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3)) self.lower_game_grid.add_widget(bottom_image) self.bottom_image_grid[-1].append(bottom_image) top_image = TapImage(lambda y=row, x=column: self.ctx.queue_auto_move(x, y), fit_mode="fill") self.upper_game_grid.add_widget(top_image) self.top_image_grid[-1].append(top_image) # Right side: Inventory image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3)) self.lower_game_grid.add_widget(image) image2 = TapIfConfettiCannonImage(lambda: self.ctx.confetti_and_rerender(), fit_mode="fill", opacity=0) self.upper_game_grid.add_widget(image2) self.top_image_grid[-1].append(image2) def start_auto_move(self) -> None: if self.move_event is not None: self.move_event.cancel() self.ctx.do_auto_move_and_rerender() self.move_event = Clock.schedule_interval(lambda _: self.ctx.do_auto_move_and_rerender(), 0.10) def build(self) -> Layout: container = super().build() self.game_view = APQuestGameView(self.ctx.input_and_rerender) self.game_view_tab = self.add_client_tab("APQuest", self.game_view) controls = APQuestControlsView() self.add_client_tab("Controls", controls) game_container = self.game_view.ids["game_container"] self.lower_game_grid = APQuestGrid() self.upper_game_grid = APQuestGrid() self.confetti_view = ConfettiView() game_container.add_widget(self.lower_game_grid) game_container.add_widget(self.upper_game_grid) game_container.add_widget(self.confetti_view) game_container.bind(size=self.lower_game_grid.check_resize) game_container.bind(size=self.upper_game_grid.check_resize) game_container.bind(size=self.confetti_view.check_resize) volume_slider_container = VolumeSliderView() volume_slider = volume_slider_container.ids["volume_slider"] volume_slider.value = self.sound_manager.volume_percentage volume_slider.bind(value=lambda _, new_volume: self.sound_manager.set_volume_percentage(new_volume)) self.grid.add_widget(volume_slider_container, index=3) Clock.schedule_interval(lambda dt: self.confetti_view.redraw_confetti(dt), 1 / 60) return container