def _screen_from_json(json_data: Any, sprites: Dict[str, Sprite]) -> Screen: background_data = json_data.get("background") background_color = None background_tilemap = None if background_data: background_color = background_data.get("background_color") tilemap_data = background_data.get("background_tilemap") if tilemap_data: background_tilemap = Tilemap(tilemap_id=tilemap_data["tilemap_id"], rect_uv=Rect.from_list( tilemap_data["tilemap_uv"])) screen_elements = [] if json_data.get("elements"): screen_elements = [ ScreenElement(position=Point.from_list(data["position"]), sprite=sprites.get(data.get("sprite_ref")), text=data.get("text")) for data in json_data["elements"] ] menu_position = None menu_scrollbar_rect = None if json_data.get("menu"): menu_data = json_data["menu"] menu_position = \ Point.from_list(menu_data["position"]) if menu_data.get("position") else None menu_scrollbar_rect = \ Rect.from_list(menu_data["scrollbar_rect"]) if menu_data.get("scrollbar_rect") else None return Screen(background_color=background_color, background_tilemap=background_tilemap, elements=tuple(screen_elements), menu_position=menu_position, menu_scrollbar_rect=menu_scrollbar_rect)
def draw(self, position: Point, selected: bool = False) -> None: draw_text( position.offset( self._get_position(GuiPosition.LEVEL_ITEM_TITLE_POS)), f"LEVEL {self.level_num}", self.text_style) if not self.level_unlocked: self._get_sprite(GuiSprite.LOCKED_ICON).draw( position.offset( self._get_position(GuiPosition.LEVEL_LOCKED_ICON_POS))) else: self._draw_level_thumbnail( position.offset( self._get_position(GuiPosition.LEVEL_THUMBNAIL_POS))) if self.level_completed: self._get_sprite(GuiSprite.CHECKED_ICON).draw( position.offset( self._get_position(GuiPosition.LEVEL_COMPLETED_ICON_POS))) if selected: self._draw_level_score( self._get_position(GuiPosition.LEVEL_SCORE_POS)) self._draw_frame(position, selected)
def __init__(self, level_num: int, raw_data: List[List[Tile]]) -> None: self.level_num = level_num self.tilemap_data = [Tile.VOID] * (LEVEL_WIDTH * LEVEL_HEIGHT) self.size = Size(len(raw_data[0]), len(raw_data)) offset = Point((LEVEL_WIDTH - self.size.width) // 2, (LEVEL_HEIGHT - self.size.height) // 2) for y in range(self.size.height): for x in range(self.size.width): pos = Point(x, y).offset(offset) self.tilemap_data[self._pos_to_offset(pos)] = raw_data[y][x]
def with_defaults(cls, items: Tuple[MenuItem, ...], layout: MenuLayout = MenuLayout()) -> "Menu": """Construct menu based on defaults. :param items: collection of menu items :param layout: layout information of the menu :return: newly created instance of Menu """ item_size = reduce(max_size, [item.size for item in items]) total_rows = -(-len(items) // layout.columns) calculated_rows = min(layout.rows, total_rows) if layout.rows else total_rows calculated_size = Size( layout.columns * item_size.width + (layout.columns - 1) * layout.item_space.width, calculated_rows * item_size.height + (calculated_rows - 1) * layout.item_space.height) centered_position = center_in_rect(calculated_size).position if layout.position: x = layout.position.x if layout.position.x >= 0 else centered_position.x y = layout.position.y if layout.position.y >= 0 else centered_position.y menu_position = Point(x, y) else: menu_position = centered_position return Menu( items=items, item_size=item_size, item_space=layout.item_space, columns=layout.columns, rows=calculated_rows, position=menu_position)
def tile_positions(self) -> Generator[Tuple[Point, Point], None, None]: """Generator for iterating over all valid tile positions inside both level and Pyxel's mega-tilemap (from top-left to bottom-right).""" tilemap_rect = tilemap_rect_nth(self.level_num) tilemap_points = tilemap_rect.inside_points() for point in tilemap_points: yield point.offset(Point(-tilemap_rect.x, -tilemap_rect.y)), point
def move(self, direction: Direction) -> None: """Move object position in given direction by one tile. Additionally reset offset. :param direction: direction of the movement """ self.tile_position = self.tile_position.move(direction) self.offset = Point(0, 0)
def draw(self, draw_as_secondary: bool = False) -> None: super().draw(draw_as_secondary) for i, item in enumerate(self.visible_items): item_space = self.menu.item_space calculated_item_size = self.menu.item_size.enlarge(item_space.width, item_space.height) position = self.menu.position.offset(Point( (i % self.menu.columns) * calculated_item_size.width, (i // self.menu.columns) * calculated_item_size.height)) item.draw(position, (self.top_row * self.menu.columns) + i == self.selected_item)
def update(self, dt_in_ms: float, stats: GameStats) -> Optional[GameAction]: running_action = super().update(dt_in_ms, stats) if running_action: move_direction = self.direction.opposite if self.backward else self.direction delta = self.elapsed_time / self.time_to_complete * TILE_SIZE self.game_object.position.offset = Point( int(delta * move_direction.dx), int(delta * move_direction.dy)) return running_action
def _draw_digits(self, gui_position: GuiPosition, text: str, gui_sprite: GuiSprite, colon_size: Optional[int] = None) -> None: position = self._get_position(gui_position) sprite = self._get_sprite(gui_sprite) char_pos = position for char in text: if char.isdigit(): sprite.draw(position=char_pos, frame=int(char)) char_size = colon_size if (char == ":" and colon_size) else sprite.width char_pos = char_pos.offset(Point(char_size + 1, 0))
def create_level_templates(json_data: Any, sprite_packs: Dict[str, SpritePack]) \ -> Tuple[LevelTemplate, ...]: """Create level templates from metadata. :param json_data: input JSON containing level template metadata :param sprite_packs: collection of available sprite packs :return: collection of level templates """ return tuple([ LevelTemplate.from_level_num( level_num=level_num, tileset_index=data["tileset"], draw_offset=Point.from_list(data["draw_offset"]), sprite_packs=LevelSpritePacks( robot_sprite_pack=sprite_packs[data["robot_sprite_pack_ref"]], crate_sprite_pack=sprite_packs[data["crate_sprite_pack_ref"]])) for level_num, data in enumerate(json_data) ])
def create_gui_consts(json_data: Any, sprites: Dict[str, Sprite]) -> GuiConsts: """Create Gui constants from metadata. :param json_data: input JSON containing Gui constants :param sprites: collection of available sprites :return: Gui constants """ return GuiConsts(gui_positions=tuple([ Point.from_list(json_data["positions"][pos.resource_name]) for pos in list(GuiPosition) ]), gui_colors=tuple([ int(json_data["colors"][color.resource_name]) for color in list(GuiColor) ]), gui_sprites=tuple([ sprites[json_data["sprites"][sprite.resource_name]] for sprite in list(GuiSprite) ]))
class ObjectPosition: """Position of game object in the level. Attributes: tile_position - game object position in tilemap space offset - position offset relative to tile_position (expressed in pixels) """ tile_position: TilePosition offset: Point = Point(0, 0) def move(self, direction: Direction) -> None: """Move object position in given direction by one tile. Additionally reset offset. :param direction: direction of the movement """ self.tile_position = self.tile_position.move(direction) self.offset = Point(0, 0) def to_point(self) -> Point: """Convert tile position to a point in screen space (taking into account the offset).""" return self.tile_position.to_point().offset(self.offset)
def draw(self, position: Point, layer: Optional[Layer] = None, direction: Direction = Direction.UP, frame: int = 0) -> None: """Draw the sprite at given position using specified layer, direction and frame. If sprite is single-layered then it will be drawn _only on main_ layer. :param position: position of sprite to be drawn at :param layer: layer of sprite to be drawn at :param direction: direction-specific variant of sprite to be drawn :param frame: frame of sprite to be drawn """ if layer and layer.layer_index >= self.num_layers: return clamped_frame = min(frame, self.num_frames - 1) frame_offset_v = clamped_frame * self.uv_rect.h // self.num_frames top_layer_offset = self.num_layers - 1 u = self.uv_rect.x + top_layer_offset v = self.uv_rect.y + frame_offset_v + top_layer_offset if self.directional: u += self.uv_rect.w // Direction.num_directions( ) * direction.direction_index if layer: u -= layer.layer_index v -= layer.layer_index offset = layer.offset if layer else Point(0, 0) pyxel.blt(position.x + offset.x, position.y + offset.y, self.image_bank, u, v, self.width, self.height, self.transparency_color)
def tilemap_offset(self) -> Point: """Offset of the tilemap, for properly centering on the screen.""" return Point( (self.size.width % 2) * TILE_SIZE // 2, (self.size.height % 2) * TILE_SIZE // 2)
def _offset_to_pos(offset: int) -> Point: return Point(offset % LEVEL_WIDTH, offset // LEVEL_WIDTH)
def draw(self, draw_as_secondary: bool = False) -> None: super().draw(draw_as_secondary) draw_text(Point(79, 240), "(c) 2020 KRZYSZTOF FURTAK", TextStyle(color=7, shadow_color=1)) draw_text(Point(11, 240), f"v{__version__}", TextStyle(color=1))
def to_point(self) -> Point: """Convert tile position to a point in screen space.""" return Point(self.tile_x * TILE_SIZE, self.tile_y * TILE_SIZE)
"""Module for handling critical game errors.""" import textwrap import pyxel from bansoko import GAME_FRAME_TIME_IN_MS from bansoko.graphics import Point, center_in_rect, Rect from bansoko.graphics.text import draw_text, text_size from bansoko.gui.menu import MenuController, Menu, TextMenuItem, MenuLayout from bansoko.gui.navigator import ScreenNavigator PADDING = Point(8, 8) class ErrorScreen(MenuController): """Screen controller for displaying game critical errors. It's displayed when a non-recoverable error occurs during game startup. """ def __init__(self, message: str): self.error_message = self._build_error_message(message) self.frame_rect = self._get_frame_rect(self.error_message) menu = Menu.with_defaults( tuple([TextMenuItem("OK", lambda: None)]), MenuLayout(position=self._get_menu_position(self.frame_rect))) super().__init__(menu=menu) def draw(self, draw_as_secondary: bool = False) -> None: self._draw_background() self._draw_frame() super().draw(draw_as_secondary=draw_as_secondary)
"""Module defining game screen which is displayed when level is completed.""" from typing import Tuple from bansoko.game.profile import LevelScore from bansoko.game.screens.screen_factory import ScreenFactory from bansoko.graphics import Point from bansoko.graphics.text import draw_text from bansoko.gui.menu import MenuController, TextMenuItem, MenuItem, Menu, MenuLayout LEVEL_TIME_POS = Point(104, 75) LEVEL_PUSHES_POS = Point(104, 84) LEVEL_STEPS_POS = Point(104, 93) class LevelCompletedController(MenuController): """Screen controller allowing player to choose what to do after completion of the level. Player can navigate to the next level, replay the current level (to get better score) or get back to Main Menu. """ def __init__(self, screen_factory: ScreenFactory, level_score: LevelScore): current_level_num = level_score.level_num last_level_completed = current_level_num == screen_factory.get_bundle( ).last_level next_level = TextMenuItem( "PLAY NEXT LEVEL", lambda: screen_factory.get_playfield_screen( screen_factory.get_player_profile().next_level_to_play( current_level_num))) finish_game = TextMenuItem("FINISH GAME", screen_factory.get_victory_screen)
def _get_menu_position(frame_rect: Rect) -> Point: return Point(-1, frame_rect.bottom - PADDING.y - pyxel.FONT_HEIGHT)