This repository has been archived on 2025-07-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
shroom_of_doom/lib/level.py
2025-02-26 14:40:25 -05:00

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()