572 lines
20 KiB
Python
572 lines
20 KiB
Python
#!~/.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() |