#!~/.pyenv/versions/3.11.6/bin/python # # Copyright (c) 2024 Cutieguwu | Olivia Brooks # # -*- coding:utf-8 -*- # @Title: Levels in a game. # @Author: Cutieguwu | Olivia Brooks # @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca # @Description: Classes descibing different levels in a pygame game. # # @Script: level.py # @Date Created: 16 Apr, 2024 # @Last Modified: 19 Jun, 2024 # @Last Modified by: Cutieguwu | Olivia Brooks # ---------------------------------------------------------- import pygame from pygame import mixer from pygame.image import load from icecream import ic from lib.system import DIRWORKING, FEATURES from lib.section import Section, Blocks from lib.interface import Button, Checkbox from lib.entity import * # Bad practise and loads player class, but loads all entities for easy reference. class Level(): """ Creates and manages the level scape. """ def __init__(self, game): self.GAME = game self.WINDOW = self.GAME.WINDOW self.SCALE = self.GAME.SCALE self.SECTION = Section(self.GAME) self.FRAMES = Frames(self.GAME) self.layout = Menus.Blank() self.dx = 0 self.dy = 0 self.musicStartTime = 0 if self.layout.TYPE == "level": self.get_level_size() def draw(self): """ Draws the level on the screen. """ if self.layout.TYPE == "level": self.SECTION.draw(self.layout.LAYOUT) elif self.layout.TYPE == "scene": self.FRAMES.draw() try: if pygame.time.get_ticks() - self.musicStartTime >= self.layout.MUSIC.get_length() * 1000: ic(self.layout.MUSIC.play()) self.musicStartTime = pygame.time.get_ticks() except AttributeError: pass def load_layout(self, layout:object, startPositionKey="default"): """ Load a new layout configuration. """ self.GAME.playerActionMove = "neutral" try: ic(self.layout.MUSIC.stop()) except AttributeError: pass if self.layout.TYPE == "menu": for interactable in self.layout.INTERACTABLES: interactable.remove() layout = layout(game=self.GAME) if isinstance(layout, Menus.Pause): # Save the level state before loading something that isn't the next level. self.save_level_state() if self.layout.TYPE == "level" and layout.TYPE != "level": self.GAME.levelOn = False elif self.layout.TYPE == "menu" and layout.TYPE != "menu": self.GAME.menuOn = False elif self.layout.TYPE == "scene" and layout.TYPE != "scene": self.GAME.sceneOn = False if self.layout.TYPE == "level": for entity in self.GAME.entityList: entity.destroy() self.layout = layout ic(self.layout.__class__) if self.layout.TYPE == "level": self.update_level_size() self.GAME.PLAYER.health = 100 self.GAME.PLAYER.effects = [] self.GAME.PLAYER.x = self.layout.STARTPOSITIONS[startPositionKey][0] self.GAME.PLAYER.y = self.layout.STARTPOSITIONS[startPositionKey][1] self.update_deltas_relative_to_player() elif self.layout.TYPE == "scene": self.FRAMES.load_images() # The following shouldn't need to exist, however pygame does not like using a low musicStartTime to trigger the first audio playback. try: self.layout.MUSIC.play() self.musicStartTime = pygame.time.get_ticks() except AttributeError: pass def load_level_state(self): """ Loads the last state of a level. """ self.layout = self.layoutOld self.GAME.entityList = self.entityOld self.GAME.levelLoad = True def save_level_state(self): """ Saves the last state of a level. """ self.layoutOld = self.layout self.entityOld = self.GAME.entityList def update_deltas_relative_to_player(self): """ Called with a scale update. Recalculates self.dx and self.dy so that player is centered properly. """ borderWidthX = (self.SCALE.gameX // 2) + 1 borderWidthY = (self.SCALE.gameY // 2) + 1 if self.GAME.PLAYER.x < borderWidthX: # Player is in left border self.dx = 0 elif self.GAME.PLAYER.x > self.width - borderWidthX: # Player is in right border self.dx = self.width - self.SCALE.gameX else: # Player is within x boundaries self.dx = self.GAME.PLAYER.x - borderWidthX if self.GAME.PLAYER.y < borderWidthY: # Player is in the top border self.dy = 0 elif self.GAME.PLAYER.y > self.height - borderWidthY: # Player is in bottom border. self.dy = self.height - self.SCALE.gameY else: # Player is within y boundaries. self.dy = self.GAME.PLAYER.y - borderWidthY self.dx = int(self.dx) self.dy = int(self.dy) def update_level_size(self): """ Updates the size of the active layout """ width = 0 height = 0 for coords, block in self.layout.LAYOUT.items(): try: if coords[2] > width: width = coords[2] if coords[3] > height: height = coords[3] except IndexError: if coords[0] > width: width = coords[0] if coords[1] > height: height = coords[1] self.width = width self.height = height """ NAME :str HAS_PLAYER :bool TYPE :str = "scene", "menu", "level" scene # Does not scale with scaleGame or scaleUi # Each obj in LAYOUT is a different frame, not block sections. # Does not have a player, so HAS_PLAYER is not checked, and can be removed from definition. menu # Does not scale with scaleGame # Has a custom event handler used while menu is active. Handler defined in run(). LAYOUT :dict = {(x, y, rangeX, rangeY): obj} BGFILL :tuple = (r, g, b) :pygame.surface.Surface FOLLOW_UP_LEVEL :level.Levels, level.Menus, level.Scenes """ class Scenes(): """ Class for organising scenes. """ """ class Example(): def __init__(self): self.IS_SKIPPABLE = True self.LAYOUT = [ { "frame": Textures.EXAMPLE_0, # Frame image "frameTime": 5 # Time in seconds that this frame is displayed. }, { "frame": Textures.EXAMPLE_1, "frameTime": 2 } ] """ class Intro(): def __init__(self, **kwargs): self.TYPE = "scene" self.LAYOUT = [ DIRWORKING + "/data/image/game/blocks/wool_colored_blue.png", DIRWORKING + "/data/image/game/blocks/wool_colored_cyan.png", DIRWORKING + "/data/image/game/blocks/wool_colored_light_blue.png" ] self.FOLLOW_UP_LEVEL = Menus.Main self.IS_SKIPPABLE = True self.TIME_PER_FRAME = 1 self.BG_FILL = (0, 0, 0) class Commencing_Slaughter(): def __init__(self, **kwargs): self.TYPE = "scene" self.LAYOUT = [ DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_4.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png" ] self.FOLLOW_UP_LEVEL = Levels.Example self.IS_SKIPPABLE = False self.TIME_PER_FRAME = 0.25 self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/scenes/commencing_slaughter.mp3") class Player_Died(): def __init__(self, **kwargs): self.TYPE = "scene" self.LAYOUT = [ DIRWORKING + "/data/image/game/blocks/wool_colored_red.png" ] self.FOLLOW_UP_LEVEL = Menus.Player_Died self.IS_SKIPPABLE = False self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav") self.TIME_PER_FRAME = self.MUSIC.get_length() class Player_Won(): def __init__(self, **kwargs): self.TYPE = "scene" self.LAYOUT = [ DIRWORKING + "/data/image/game/blocks/wool_colored_lime.png" ] self.FOLLOW_UP_LEVEL = Menus.Player_Won self.IS_SKIPPABLE = True self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav") self.TIME_PER_FRAME = self.MUSIC.get_length() class Menus(): """ Class for organising menus. """ class Blank(): def __init__(self, **kwargs): self.TYPE = "menu" self.HAS_PLAYER = False self.INTERACTABLES = [] self.BG_FILL = (0, 0, 0) class Main(): def __init__(self, game): self.TYPE = "menu" self.HAS_PLAYER = False self.INTERACTABLES = [ Button( game, "start", 100, 100, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.LEVEL.load_layout, args=Scenes.Commencing_Slaughter ), Button( game, "settings", 100, 200, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.LEVEL.load_layout, #args=Menus.Settings args=Menus.Main ), Button( game, "exit_to_desktop", 100, 300, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.MENU_FUNCTIONS.exit_to_desktop ) ] self.BG_FILL = pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert() self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/ui/menu_main/Backwards-Souls-SoundBible.com-87826574.wav") class Settings(): def __init__(self, game): self.TYPE = "menu" self.HAS_PLAYER = False self.INTERACTABLES = [ Checkbox( game, "fullscreen", 100, 100, pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_hover.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_active.png").convert_alpha(), lambda: FEATURES["fullscreen"]["is_active"], # Needs to be re-evaluated. Thus lambda function. game.MENU_FUNCTIONS.enter_fullscreen ) ] self.BG_FILL = pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert() class Pause(): def __init__(self, game): self.TYPE = "menu" self.HAS_PLAYER = False self.INTERACTABLES = [ Button( game, "return_to_game", 100, 100, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.MENU_FUNCTIONS.return_to_game ), Button( game, "settings", 100, 200, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.MENU_FUNCTIONS.return_to_game #game.LEVEL.load_layout, #args=Menus.Settings ), Button( game, "exit_to_menu", 100, 300, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.LEVEL.load_layout, args=Menus.Main ) ] # Save a snapshot of the window to overlay the pause menu on. pygame.image.save(game.WINDOW.window, DIRWORKING + "/temp/pausescreen.png") self.BG_FILL = pygame.image.load(DIRWORKING + "/temp/pausescreen.png").convert() class Player_Died(): def __init__(self, game): self.TYPE = "menu" self.HAS_PLAYER = False self.INTERACTABLES = [ Button( game, "exit_to_menu", 100, 300, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.LEVEL.load_layout, args=Menus.Main ) ] self.BG_FILL = load(DIRWORKING + "/data/image/game/blocks/wool_colored_red.png") class Player_Won(): def __init__(self, game): self.TYPE = "menu" self.HAS_PLAYER = False self.INTERACTABLES = [ Button( game, "exit_to_menu", 100, 300, pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), game.LEVEL.load_layout, args=Menus.Main ) ] self.BG_FILL = load(DIRWORKING + "/data/image/game/blocks/wool_colored_lime.png") class Levels(): """ Class for organising levels. """ class Example(): def __init__(self, game, **kwargs): self.TYPE = "level" self.HAS_PLAYER = True self.LAYOUT = { (0, 0, 96, 64): Blocks.block_blue(), (2, 1): Blocks.block_rainbow(layer=1), (14, 5): Blocks.block_rainbow(layer=1), (3, 3, 5, 5): Blocks.puddle_of_souls(game, layer=1), (7, 7): Blocks.puddle_of_souls(game, layer=1), (4, 9): Blocks.puddle_of_souls(game, layer=1) } self.ENTITIES = [ Unnamed(game, 16, 16), Unnamed(game, 16, 17), Unnamed(game, 17, 16), Unnamed(game, 17, 17), Unnamed(game, 15, 23), Unnamed(game, 41, 52), Unnamed(game, 5, 17) ] self.STARTPOSITIONS = {"default": (0, 0)} self.BG_FILL = (0, 0, 0) self.FOLLOW_UP_LEVEL = Scenes.Player_Won class Frames(): """ Class for methods pertaining to scenes. """ def __init__(self, game): self.GAME = game self.WINDOW = self.GAME.WINDOW self.clear() def clear(self): """ Clear the frames from memory. """ self.images = [ pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert() ] self.imageNumber = -1 self.frames = 0 # Frames rendered by system def draw(self): """ Draws the scene. """ if self.frames / self.GAME.clock.get_fps() > self.GAME.LEVEL.layout.TIME_PER_FRAME: # Use number of frames displaying an image / framerate to get time in sec. self.imageNumber = self.imageNumber + 1 # Increment the image index self.frames = 0 if len(self.images) == self.imageNumber: self.GAME.LEVEL.load_layout(self.GAME.LEVEL.layout.FOLLOW_UP_LEVEL) self.clear() else: self.WINDOW.window.blit(self.images[self.imageNumber], (0, 0)) elif self.imageNumber == -1: self.imageNumber = 0 self.WINDOW.window.blit(self.images[self.imageNumber], (0, 0)) else: self.frames = self.frames + 1 def load_images(self): """ Load frames for a scene. """ self.images = [] for path in self.GAME.LEVEL.layout.LAYOUT: self.images.append(load(path).convert_alpha()) self.scale_images() def scale_images(self): """ Scales all images currently loaded. """ for image in self.images: self.images[self.images.index(image)] = pygame.transform.scale(image, (self.WINDOW.width, self.WINDOW.height)) def skip(self): """ Skips the level. """ self.clear() self.GAME.LEVEL.load_layout(self.GAME.LEVEL.layout.FOLLOW_UP_LEVEL) def update_scale(self): """ Updates the scaling of the active frames. """ if self.WINDOW.width > self.images[0].get_width() or self.WINDOW.height > self.images[0].get_height(): self.load_images() else: self.scale_images()