Initial Commit

This commit is contained in:
Cutieguwu
2025-02-26 14:40:25 -05:00
commit d9dc0e390c
129 changed files with 4106 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

51
lib/effect.py Normal file
View File

@@ -0,0 +1,51 @@
#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: Intro to Pygame.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: Effects for entities.
#
# @Script: effect.py
# @Date Created: 11 Jun, 2024
# @Last Modified: 11 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------
from pygame.image import load
from pygame.transform import scale
from lib.system import DIRWORKING
class War_Paint():
def __init__(self, game, character):
"""
War paint for player.
"""
self.GAME = game
self.imagePaths = {}
self.images = {}
self.update_images(character)
def function():
"""
War Paint is just a texture effect.
"""
pass
def update_images(self, character):
"""
Updates effect images.
"""
for key in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]:
self.imagePaths[key] = DIRWORKING + f"/data/image/game/player/{character}/war_paint/{key}.png"
for key, value in self.imagePaths.items():
self.images[key] = load(self.imagePaths[key]).convert_alpha()
self.images[key] = scale(self.images[key], (self.GAME.SCALE.unitBlockPx, self.GAME.SCALE.unitBlockPx))

617
lib/entity.py Normal file
View File

@@ -0,0 +1,617 @@
#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: Players and Characters.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: AIs for players and characters.
#
# @Script: entity.py
# @Date Created: 22 Mar, 2024
# @Last Modified: 19 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------
import pygame
from pygame import mixer
from icecream import ic
from random import randint
from lib.system import DIRWORKING, FEATURES
from lib.section import Blocks
class Player():
"""
Handles the player character and associated animations and states.
"""
def __init__(self, game, character:str = "default_a"):
self.GAME = game
self.WINDOW = self.GAME.WINDOW
self.SCALE = self.GAME.SCALE
self.LEVEL = self.GAME.LEVEL
self.images = {}
self.x = 0
self.y = 0
self.layer = 1
self.health = 100
self.effects = []
self.update_images(character)
self.animationClock = 0
self.stateDirection = "neutral" # String of character state
self.stateDirectionOld = "run_d" # Last movement action by player.
self.stateDirectionOldX = "run_l" # Last x movement action for rotating to neutral from run_u
self.stateFrame = 1 # Animation frame number
self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] # Default inital character state.
self.timeSinceInteraction = 0
self.moveClock = 0
self.update_scale()
def adjust_center(self, x, y):
"""
Determines if the player can be centered on the level instead of shifting position from center.
"""
# if newPosition is greater than half the viewable area relative to dx if it were adjusted.
# less efficient:
# if dx were adjusted, if any wall becomes only void, adjust player position.
dCenterX = self.SCALE.gameX / 2
dCenterY = self.SCALE.gameY / 2
if x <= dCenterX and self.LEVEL.dx == 0:
# Player in left region of layout.
# Player must move.
pass
elif x > (self.LEVEL.width - dCenterX) + 1:
# Player in right region of layout.
# Player must move.
pass
else:
# Level should move.
newDx = self.LEVEL.dx + (x - self.x)
if newDx >= 0:
self.LEVEL.dx = newDx
if y <= dCenterY and self.LEVEL.dy == 0:
# Player in top region of layout
# Player must move.
pass
elif y > (self.LEVEL.height - dCenterY) + 1:
# Player in bottom region of layout
# Player must move.
pass
else:
# Level should move.
newDy = self.LEVEL.dy + (y - self.y)
if newDy >= 0:
self.LEVEL.dy = newDy
self.x = x
self.y = y
def attack(self):
"""
Makes the player attack.
"""
attackPositions = []
if self.stateDirection == "run_l":
x = self.x - 1
attackPositions.append((x, self.y - 1))
attackPositions.append((x, self.y))
attackPositions.append((x, self.y + 1))
elif self.stateDirection == "run_r":
x = self.x + 1
attackPositions.append((x, self.y - 1))
attackPositions.append((x, self.y))
attackPositions.append((x, self.y + 1))
elif self.stateDirection == "run_u":
y = self.y - 1
attackPositions.append((self.x - 1, y))
attackPositions.append((self.x, y))
attackPositions.append((self.x + 1, y))
elif self.stateDirection == "run_d":
y = self.y + 1
attackPositions.append((self.x - 1, y))
attackPositions.append((self.x, y))
attackPositions.append((self.x + 1, y))
for entity in self.GAME.entityList:
if (entity.x, entity.y) in attackPositions:
entity.health = entity.health - 25
def can_interact(self, x, y):
"""
Checks if the player can interact with the section in front of it.
"""
try: # Try to set the section based on coords.
coords = (x, y)
section = self.LEVEL.layout.LAYOUT[coords]
except KeyError:
for coords, block in self.LEVEL.layout.LAYOUT.items(): # Deeper search, check if block in an area fill.
if block.layer == self.layer:
try:
if y in range(coords[1], coords[3] + 1):
if x in range(coords[0], coords[2] + 1):
section = self.LEVEL.layout.LAYOUT[coords] # Set the definition as the desired section.
break
except IndexError: # Block doesn't define a range.
pass
try:
if section.TEXTURE[section.state]["IS_INTERACTABLE"]:
return coords
else:
return False
except UnboundLocalError: # Could not find a block drawn at those coords.
return False
def can_move(self, x, y):
"""
Checks if the player can move to a position.
"""
try: # Try to set the section based on coords.
section = self.LEVEL.layout.LAYOUT[(x, y)]
except KeyError:
layerLast = -1
for coords, block in self.LEVEL.layout.LAYOUT.items(): # Deeper search, check if block in an area fill.
if block.layer <= self.layer and block.layer >= layerLast:
try:
if y in range(coords[1], coords[3] + 1):
if x in range(coords[0], coords[2] + 1):
section = self.LEVEL.layout.LAYOUT[coords] # Set the definition as the desired section.
layerLast = block.layer
except IndexError: # Block doesn't define a range.
pass
try:
if section.TEXTURE[section.state]["IS_COLLIDABLE"]:
return False
else:
for entity in self.GAME.entityList:
if entity.x == x and entity.y == y: # If there is an entity at this location, cannot move.
return False
return True
except UnboundLocalError: # Could not find a block drawn at those coords.
return False
def dash(self, direction):
"""
Dashes player in gameX and gameY.
"""
x, y = self.x, self.y
dashDist = 0
canDash = False
# If Player is not moving to same position as currently located
while dashDist != 5:
dashDist = dashDist + 1
if FEATURES["matchcase"]["is_active"]:
match direction:
case "run_l":
x = self.x - dashDist
self.stateDirectionOldX = direction
case "run_r":
x = self.x + dashDist
self.stateDirectionOldX = direction
case "run_u":
y = self.y - dashDist
case "run_d":
y = self.y + dashDist
# Iterate until player is trying to move to a position that it can move.
elif direction == "run_l":
x = self.x - dashDist
self.stateDirectionOldX = direction
elif direction == "run_r":
x = self.x + dashDist
self.stateDirectionOldX = direction
elif direction == "run_u":
y = self.y - dashDist
elif direction == "run_d":
y = self.y + dashDist
if self.can_move(x, y):
canDash = True
moveX, moveY = x, y
else: # Prevent warping through blocks if dashing immediately against them.
moveX, moveY = self.x, self.y
canDash = False
break
if canDash:
self.adjust_center(moveX, moveY)
self.moveClock = 0
self.update_animation(direction, is_updateMove = True)
def draw(self, direction):
"""
Draw the player on the screen.
"""
self.update_move(direction)
location = ((self.x - self.LEVEL.dx) * self.SCALE.unitBlockPx, (self.y - self.LEVEL.dy) * self.SCALE.unitBlockPx)
self.WINDOW.window.blit(self.stateImage, location)
for effect in self.effects:
self.WINDOW.window.blit(effect.images[f"{self.stateDirection}_{self.stateFrame}"], location)
def interact(self):
"""
Interacts with the object in front of the player.
"""
x = self.x
y = self.y
if self.stateDirection == "neutral":
if self.stateDirectionOld == "run_l":
x = self.x - 1
elif self.stateDirectionOld == "run_r":
x = self.x + 1
elif self.stateDirectionOld == "run_u":
y = self.y - 1
elif self.stateDirectionOld == "run_d":
y = self.y + 1
else:
if self.stateDirection == "run_l":
x = self.x - 1
elif self.stateDirection == "run_r":
x = self.x + 1
elif self.stateDirection == "run_u":
y = self.y - 1
elif self.stateDirection == "run_d":
y = self.y + 1
coords = self.can_interact(x, y)
if isinstance(coords, tuple):
section = self.LEVEL.layout.LAYOUT[coords]
if section.layer == self.layer:
section.on_interact()
def update_animation(self, direction: str = None, is_updateMove = False, ovrrScaling = False):
"""
Sets the current animation image of the player.
"""
self.animationClock = self.animationClock + 1
# Check time since interacted.
if is_updateMove == True:
self.timeSinceInteraction = 0
elif self.stateDirection != "neutral" and is_updateMove == False:
self.timeSinceInteraction = self.timeSinceInteraction + 1
if ovrrScaling: # Replace current image with scaled.
self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]
elif self.animationClock >= (self.GAME.clock.get_fps() / 3) or (direction is not None and self.stateDirection != direction):
# Update animation every half a second
# Or when direction has changed.
if self.stateFrame == 1:
self.stateFrame = 2
elif self.stateFrame == 2:
self.stateFrame = 1
# Change Direction
if direction is not None and self.stateDirection != direction: # If direction has changed, update the animation at the same time.
self.stateDirection = direction
# Make player idle if not moving for 3 frame updates.
if self.timeSinceInteraction > (3 * (self.GAME.clock.get_fps() / 2)):
if self.stateDirection == "run_u":
self.stateDirection = self.stateDirectionOldX
#ic("Returning to neutral via the last L/R direction")
else:
self.stateDirection = "neutral"
self.timeSinceInteraction = 0
#ic("Returning to neutral and resetting.")
self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]
self.animationClock = 0
def update_images(self, character):
"""
Updates animation images based on the character.
"""
self.character = character
for key in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]:
self.images[key] = pygame.image.load(DIRWORKING + f"/data/image/game/player/{self.character}/base/{key}.png").convert_alpha()
for effect in self.effects:
effect.update_images(self.character)
def update_move(self, direction:str):
"""
Moves player in gameX and gameY.
"""
x = self.x
y = self.y
self.stateDirectionOld = self.stateDirection
if direction == "run_l":
x = self.x - 1
self.stateDirectionOldX = direction
elif direction == "run_r":
x = self.x + 1
self.stateDirectionOldX = direction
elif direction == "run_u":
y = self.y - 1
elif direction == "run_d":
y = self.y + 1
if direction != "neutral":
if self.can_move(x, y) and self.moveClock >= self.GAME.clock.get_fps() / 30:
self.adjust_center(x, y)
self.moveClock = 0
else:
self.moveClock = self.moveClock + 1
self.update_animation(direction, is_updateMove = True)
else:
self.update_animation()
def update_scale(self):
"""
Updates the scaling of all player textures.
"""
self.update_images(self.character)
self.rect = (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx)
# Scale each image
for key, value in self.images.items():
self.images[key] = pygame.transform.scale(value, self.rect)
for effect in self.effects:
effect.update_scale(self.character)
self.update_animation(ovrrScaling=True)
class Unnamed():
"""
Spawns one of The Unnamed.
"""
def __init__(self, game, x, y, layer=1):
self.GAME = game
self.WINDOW = self.GAME.WINDOW
self.SCALE = self.GAME.SCALE
self.LEVEL = self.GAME.LEVEL
self.PLAYER = self.GAME.PLAYER
self.images = {}
self.x = x
self.y = y
self.layer = layer
self.health = 25
for k in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]:
self.images[k] = ""
self.update_images()
self.animationClock = 0
self.stateDirection = "neutral" # String of character state
self.stateDirectionOld = "run_d" # Last movement action by player.
self.stateFrame = 1 # Animation frame number
self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] # Default inital character state.
self.timeSinceInteraction = 0
self.actionClock = 0
self.update_scale()
self.GAME.entityList.append(self)
def attack(self):
"""
Makes the unnamed attack player.
"""
self.PLAYER.health = self.PLAYER.health - 25
self.actionClock = pygame.time.get_ticks()
def calc_x(self):
"""
Calculates an x value movement based on PLAYER.
"""
if self.x > self.PLAYER.x:
self.stateDirectionOld = self.stateDirection
self.stateDirection = "run_l"
return self.x - 1, self.y
elif self.x < self.PLAYER.x:
self.stateDirectionOld = self.stateDirection
self.stateDirection = "run_r"
return self.x + 1, self.y
elif self.x == self.PLAYER.x and self.y == self.PLAYER.y:
return self.x, self.y
else:
return self.calc_y()
def calc_y(self):
"""
Calculates a y value movement based on PLAYER.
"""
if self.y > self.PLAYER.y:
self.stateDirectionOld = self.stateDirection
self.stateDirection = "run_u"
return self.x, self.y - 1
elif self.y < self.PLAYER.y:
self.stateDirectionOld = self.stateDirection
self.stateDirection = "run_d"
return self.x, self.y + 1
elif self.x == self.PLAYER.x and self.y == self.PLAYER.y:
return self.x, self.y
else:
return self.calc_x()
def can_move(self, x:int, y:int):
"""
Checks if the player can move to a position.
"""
try: # Try to set the section based on coords.
section = self.LEVEL.layout.LAYOUT[(x, y)]
except KeyError:
layerLast = -1
for coords, block in self.LEVEL.layout.LAYOUT.items(): # Deeper search, check if block in an area fill.
if block.layer <= self.layer and block.layer >= layerLast:
try:
if y in range(coords[1], coords[3] + 1):
if x in range(coords[0], coords[2] + 1):
section = self.LEVEL.layout.LAYOUT[coords] # Set the definition as the desired section.
layerLast = block.layer
except IndexError: # Block doesn't define a range.
pass
try:
if section.TEXTURE[section.state]["IS_COLLIDABLE"]:
return False
elif self.PLAYER.x == x and self.PLAYER.y == y: # If player at this location, attack.
self.attack()
return False
else:
for entity in self.GAME.entityList:
if entity.x == x and entity.y == y: # If there is an entity at this location, cannot move.
return False
return True
except UnboundLocalError: # Could not find a block drawn at those coords.
return False
def draw(self):
"""
Draw the unnamed on the screen.
"""
self.update_move()
self.update_animation()
self.WINDOW.window.blit(self.stateImage, ((self.x - self.LEVEL.dx) * self.SCALE.unitBlockPx, (self.y - self.LEVEL.dy) * self.SCALE.unitBlockPx))
def destroy(self):
"""
Removes the unnamed from the entityList.
"""
if self.GAME.levelOn and len(self.GAME.entityList) > 1:
self.LEVEL.layout.LAYOUT[(self.x, self.y)] = Blocks.puddle_of_blood(self.GAME, self.layer)
deathSound = mixer.Sound(DIRWORKING + "/data/sound/game/entity/death/jenna_cough.mp3")
deathSound.set_volume(0.2)
deathSound.play()
temp = []
for entity in self.GAME.entityList:
if entity != self:
temp.append(entity)
self.GAME.entityList = temp
del self
def update_animation(self, ovrrScaling = False):
"""
Sets the current animation image of the unnamed.
"""
self.animationClock = self.animationClock + 1
if ovrrScaling: # Replace current image with scaled.
self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]
elif self.animationClock >= (self.GAME.clock.get_fps() / 2): # Update animation every half a second.
if self.stateFrame == 1:
self.stateFrame = 2
elif self.stateFrame == 2:
self.stateFrame = 1
self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]
self.animationClock = 0
def update_images(self):
"""
Updates animation images .
"""
for key, value in self.images.items():
self.images[key] = pygame.image.load(DIRWORKING + f"/data/image/game/entity/unnamed/{key}.png").convert_alpha()
def update_move(self):
"""
Updates the position of the unnamed.
"""
if pygame.time.get_ticks() - self.actionClock >= 1000:
if bool(randint(0, 1)):
x, y = self.calc_x()
else:
x, y = self.calc_y()
if self.can_move(x, y):
self.x = x
self.y = y
self.actionClock = pygame.time.get_ticks()
else:
self.stateDirection = self.stateDirectionOld
self.update_animation()
def update_scale(self):
"""
Updates the scaling of all unnamed textures.
"""
self.update_images()
self.rect = (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx)
# Scale each image
for key, value in self.images.items():
self.images[key] = pygame.transform.scale(value, self.rect)
self.update_animation(ovrrScaling=True)

300
lib/interface.py Normal file
View File

@@ -0,0 +1,300 @@
#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: Interface Items.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: Interface items for menus and scenes in a pygame game.
#
# @Script: interface.py
# @Date Created: 22 Mar, 2024
# @Last Modified: 13 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------
import pygame
from lib.system import FEATURES
class Button():
"""
Creates a button on a window.
"""
def __init__(self, game, name, x, y, image_neutral, image_hover, function, args=None):
self.TYPE = "button"
width = image_neutral.get_width() # Can use neutral image as all images should have identical sizes.
height = image_neutral.get_height()
self.GAME = game
self.WINDOW = self.GAME.WINDOW
scale = self.GAME.SCALE.scaleUi
self.function = function
self.functionArgs = args
self.ID = name
self.IMAGES = {
"neutral": pygame.transform.scale(image_neutral, (width * scale, height * scale)),
"hover": pygame.transform.scale(image_hover, (width * scale, height * scale))
}
Interface_Functions.convert_images(self)
self.rect = self.IMAGES["neutral"].get_rect()
self.rect.topleft = (x * scale, y * scale)
# Cannot overwrite rect.collidepoint with Interface_Functions.collidepoint.
self.time_delay = 0
self.GAME.gameInteractables.append(self) # Add to button to refresh.
def draw(self):
"""
Draws button on screen.
"""
Interface_Functions.draw(self)
def is_hovered(self):
"""
Checks if button is hovered.
"""
return Interface_Functions.collidepoint(self, pygame.mouse.get_pos())
def remove(self):
"""
Clears button from screen.
"""
Interface_Functions.destroy(self)
class Progress_bar():
"""
Creates a progress bar that updates based on a scale.
"""
def __init__(self, game, x, y, barWidth:int, bobberImages:list, barImageLeft, barImageRight, barImageClear, barImageFill):
"""
x: distance in percent from window origin.
y: distance in percent from window origin.
barWidth: Percent of window width filled.
"""
self.GAME = game
self.WINDOW = self.GAME.WINDOW
self.x = (x / 100) * self.WINDOW.width
self.y = (y / 100) * self.WINDOW.height
for image in bobberImages:
image.convert_alpha()
self.bobberImages = bobberImages
self.barImages = {
"left": barImageLeft.convert_alpha(),
"right": barImageRight.convert_alpha(),
"clear": barImageClear.convert_alpha(),
"fill": barImageFill.convert_alpha()
}
self.bobberState = 0
self.bobberStateRange = len(bobberImages)
self.widthLeft = self.barImages["left"].get_width()
self.widthRight = self.barImages["right"].get_width()
self.barFillWidth = int("%.0f" % ((self.WINDOW.width * (barWidth / 100)) - (self.widthLeft + self.widthRight)))
self.barHeight = self.barImages["left"].get_height()
self.bobberHeight = self.bobberImages[self.bobberState].get_height()
self.barCenterOffset = (self.barHeight / 2) - (self.barImages["clear"].get_height() / 2)
self.bobberTimer = 0
def draw(self, rangeComplete, rangeTotal, framerate=30):
self.bobberTimer = self.bobberTimer + 1
rangeComplete = int("%.0f" % (rangeComplete * self.barFillWidth / rangeTotal))
self.WINDOW.window.blit(self.barImages["left"], (self.x, self.y))
self.WINDOW.window.blit(self.barImages["right"], (self.x + self.widthLeft + self.barFillWidth, self.y))
for i in range(0, rangeComplete):
self.WINDOW.window.blit(self.barImages["fill"], (self.x + self.widthLeft + i, self.y + self.barCenterOffset))
for i in range(rangeComplete, self.barFillWidth):
self.WINDOW.window.blit(self.barImages["clear"], (self.x + self.widthLeft + i, self.y + self.barCenterOffset))
if (self.bobberTimer / framerate) > 0.25:
self.bobberTimer = 0
if self.bobberState == self.bobberStateRange - 1:
self.bobberState = 0
else:
self.bobberState = self.bobberState + 1
self.WINDOW.window.blit(self.bobberImages[self.bobberState], (self.x + rangeComplete, self.y - (self.bobberHeight / 2)))
class Checkbox():
"""
Creates a Checkbox on a window.
"""
def __init__(self, game, name, x, y, image_neutral, image_hover, image_active, condition, function=None, args=None):
self.TYPE = "checkbox"
width = image_neutral.get_width() # Can use neutral image as all images should have identical sizes.
height = image_neutral.get_height()
self.GAME = game
self.WINDOW = self.GAME.WINDOW
scale = self.GAME.SCALE.scaleUi
self.function = function
self.functionArgs = args
self.ID = name
self.IMAGES = {
"neutral": pygame.transform.scale(image_neutral, (width * scale, height * scale)),
"hover": pygame.transform.scale(image_hover, (width * scale, height * scale)),
"active": pygame.transform.scale(image_active, (width * scale, height * scale))
}
Interface_Functions.convert_images(self)
self.rect = self.image_neutral.get_rect()
self.rect.topleft = (x * scale, y * scale)
self.time_delay = 0
self.condition = condition
self.GAME.gameInteractables.append(self) # Add to checkbox to refresh.
def draw(self):
"""
Draws checkbox on screen.
"""
Interface_Functions.draw(self)
def is_hovered(self):
"""
Checks if checkbox is hovered.
"""
return Interface_Functions.is_hovered(self)
def remove(self):
"""
Clears checkbox from screen.
"""
Interface_Functions.destroy(self)
class Interface_Functions():
"""
All functions called by interface objects.
"""
def __init__(self, game):
self.GAME = game
def convert_images(object):
"""
Converts the object's images to pygame format increasing performance.
"""
temp = {}
for image in object.IMAGES.items():
temp[image[0]] = image[1].convert_alpha()
object.IMAGES = temp
def destroy(object):
"""
Destroys the interface object.
"""
temp = []
for interactable in object.GAME.gameInteractables:
if interactable != object:
temp.append(interactable)
object.GAME.gameInteractables = temp
del object
def draw(object):
"""
Draws interface object.
"""
if object.TYPE == "checkbox" and object.condition():
object.WINDOW.window.blit(object.IMAGES["active"], (object.rect.x, object.rect.y))
elif object.is_hovered():
object.WINDOW.window.blit(object.IMAGES["hover"], (object.rect.x, object.rect.y))
else:
object.WINDOW.window.blit(object.IMAGES["neutral"], (object.rect.x, object.rect.y))
def enter_fullscreen(self):
"""
Enters Fullscreen if possible.
"""
if self.GAME.WINDOW.toggle_fullscreen():
FEATURES["Fullscreen"]["is_active"] = True
def exit_to_desktop(self):
self.GAME.levelOn = False
self.GAME.menuOn = False
self.GAME.sceneOn = False
self.GAME.gameOn = False
def collidepoint(object, coords:tuple):
"""
Rewritten version of pygame rect.collidepoint.
"""
x, y, width, height = object.rect
dx = width + x
dy = height + y
if coords[0] in range(x, dx + 1) and coords[1] in range(y, dy + 1):
return True
else:
return False
def is_hovered(object):
"""
Checks if mouse is hovering interface object.
"""
return Interface_Functions.collidepoint(object, pygame.mouse.get_pos())
def return_to_game(self):
"""
Exits a pause state.
"""
for button in self.GAME.LEVEL.layout.INTERACTABLES:
button.remove()
self.GAME.menuOn = False
self.GAME.LEVEL.load_level_state()

572
lib/level.py Normal file
View File

@@ -0,0 +1,572 @@
#!~/.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()

424
lib/section.py Normal file
View File

@@ -0,0 +1,424 @@
#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: Sections of layers in a game.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: Classes descibing different sections in a pygame game.
#
# @Script: section.py
# @Date Created: 16 Apr, 2024
# @Last Modified: 19 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------
from icecream import ic
from random import randint
from pygame.image import load
from pygame.transform import scale
from lib import effect
from lib.system import DIRWORKING
class Section():
"""
A 16x16 area of the layer.
If this section can be interacted with by a player, `Section` will store the function.
"""
def __init__(self, game):
self.GAME = game # Cannot shortcut self.LEVEL due to Section befing used as part the init
self.SCALE = game.SCALE
self.WINDOW = game.WINDOW
def draw(self, layout):
"""
Draw the section-built layout on the screen.
"""
blocksInView = {}
blocksDrawn = 0
is_sorted = False
layers = []
currentLayerIndex = 0
# Improve processing time, only blocks on display will be drawn.
# Improve engine to support drawing areas with a single block, and not blitting blocks without alpha over blocks of lower layers.
for coords, block in layout.items():
try: # Does this block define a range fill.
for y in range(coords[1], coords[3] + 1):
if y in range(self.GAME.LEVEL.dy, self.GAME.LEVEL.dy + self.SCALE.gameY + 1): # Only try to blit blocks on a displayed row.
for x in range(coords[0], coords[2] + 1):
if x in range(self.GAME.LEVEL.dx, self.GAME.LEVEL.dx + self.SCALE.gameX + 1):
blocksInView[(x, y, block.layer)] = block # Add block layer to prevent overwriting blocks underneath.
if block.layer not in layers: # Get all the layers present of displayed blocks
layers.append(block.layer)
except IndexError:
if coords[0] in range(self.GAME.LEVEL.dx, self.GAME.LEVEL.dx + self.SCALE.gameX + 1) and coords[1] in range(self.GAME.LEVEL.dy, self.GAME.LEVEL.dy + self.SCALE.gameY + 1): # If block is in display area.
blocksInView[coords] = block
if block.layer not in layers: # Get all the layers present of displayed blocks
layers.append(block.layer)
while not is_sorted: # Sort layers to draw their blocks at the correct heights.
is_sorted = True
for i in range(0, len(layers) - 1):
if layers[i] > layers[i + 1]:
is_sorted = False
temp = layers[i]
layers[i] = layers[i + 1]
layers[i + 1] = temp
while blocksDrawn < len(blocksInView): # While there are blocks still not drawn
for location, block in blocksInView.items(): # Go through each block and draw it if it is on the layer currently being drawn.
if block.layer == layers[currentLayerIndex]: # Use a list of layers so that if no blocks in view are on a layer, no time is wasted parsing blocks on a layer that doesn't show.
x = (location[0] - self.GAME.LEVEL.dx) * self.SCALE.unitBlockPx # Get its x, y location in pixels.
y = (location[1] - self.GAME.LEVEL.dy) * self.SCALE.unitBlockPx
if block.TEXTURE[block.state]["IS_ANIMATED"]: # Get the current texture frame for animated textures.
block.animationClock = block.animationClock + 1
if block.animationClock >= (self.GAME.clock.get_fps() * block.TEXTURE[block.state]["TIME_PER_FRAME"]): # If the time in seconds passed is equal or greater than the time per frame of the animated texture
block.animationClock = 0
textures = len(block.TEXTURE[block.state]["texture"])
currentTexture = block.textureIndex # Fetch value to marginally reduce time to reach value repeatedly from dict.
if (currentTexture + 1) > (textures - 1):
block.textureIndex = 0
if block.TEXTURE[block.state]["ANIMATION_REPEATS"] != -1: # If a limited animation
block.animationRepeats = block.animationRepeats + 1
if block.animationRepeats == block.TEXTURE[block.state]["ANIMATION_REPEATS"]: # Block has animated for its state's full time.
Section.update_state(block) # Cannot call self.update_state for some reason.
# Causes two positional arugments by witchcraft.
else:
block.textureIndex = currentTexture + 1
texture = block.TEXTURE[block.state]["texture"][block.textureIndex]
else: # Otherwise print its static texture for its state.
texture = block.TEXTURE[block.state]["texture"][0]
if texture.get_width() != self.SCALE.unitBlockPx:
texture = scale(texture, (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx))
self.WINDOW.window.blit(texture, (x, y))
blocksDrawn = blocksDrawn + 1
#ic(f"Printed blocks {blocksDrawn}/{len(blocksInView)}")
currentLayerIndex = currentLayerIndex + 1
def update_state(section):
"""
Updates the state of the section.
"""
section.state = section.TEXTURE[section.state]["FOLLOW_UP_STATE"]
section.textureIndex = 0
class Textures():
"""
Contains all textures used by the game.
Not the most memory-efficient. May want to rework this simple memory hog.
"""
def __init__(self, unitBlockPx):
self.load_textures()
self.update_scale(unitBlockPx)
def load_textures(self):
"""
Loads textures from file.
"""
BLOCKSDIR = DIRWORKING + "/data/image/game/blocks/"
self.TEXTURES = {
"BLOCK_BLUE": load(BLOCKSDIR + "wool_colored_blue.png").convert(),
"BLOCK_BROWN": load(BLOCKSDIR + "wool_colored_brown.png").convert(),
"BLOCK_CYAN": load(BLOCKSDIR + "wool_colored_cyan.png").convert(),
"BLOCK_GRAY": load(BLOCKSDIR + "wool_colored_gray.png").convert(),
"BLOCK_GREEN": load(BLOCKSDIR + "wool_colored_green.png").convert(),
"BLOCK_LIGHT_BLUE": load(BLOCKSDIR + "wool_colored_light_blue.png").convert(),
"BLOCK_LIME": load(BLOCKSDIR + "wool_colored_lime.png").convert(),
"BLOCK_MAGENTA": load(BLOCKSDIR + "wool_colored_magenta.png").convert(),
"BLOCK_ORANGE": load(BLOCKSDIR + "wool_colored_orange.png").convert(),
"BLOCK_PINK": load(BLOCKSDIR + "wool_colored_pink.png").convert(),
"BLOCK_PURPLE": load(BLOCKSDIR + "wool_colored_purple.png").convert(),
"BLOCK_RED": load(BLOCKSDIR + "wool_colored_red.png").convert(),
"BLOCK_SILVER": load(BLOCKSDIR + "wool_colored_silver.png").convert(),
"BLOCK_WHITE": load(BLOCKSDIR + "wool_colored_white.png").convert(),
"BLOCK_YELLOW": load(BLOCKSDIR + "wool_colored_yellow.png").convert(),
"PUDDLE_SOULS_RICH": load(BLOCKSDIR + "puddle_souls/puddle_souls_rich.png").convert_alpha(),
"PUDDLE_SOULS_SPENT": load(BLOCKSDIR + "puddle_souls/puddle_souls_spent.png").convert_alpha(),
"PUDDLE_BLOOD_0": load(BLOCKSDIR + "puddle_blood/puddle_blood_0.png").convert_alpha(),
"PUDDLE_BLOOD_1": load(BLOCKSDIR + "puddle_blood/puddle_blood_1.png").convert_alpha(),
"PUDDLE_BLOOD_2": load(BLOCKSDIR + "puddle_blood/puddle_blood_2.png").convert_alpha(),
"PUDDLE_BLOOD_3": load(BLOCKSDIR + "puddle_blood/puddle_blood_3.png").convert_alpha(),
"PUDDLE_BLOOD_4": load(BLOCKSDIR + "puddle_blood/puddle_blood_4.png").convert_alpha(),
"PUDDLE_BLOOD_NEUTRAL": load(BLOCKSDIR + "puddle_blood/puddle_blood_neutral.png").convert_alpha()
}
def update_scale(self, unitBlockPx):
"""
Updates the scaling of all textures.
"""
if unitBlockPx > self.TEXTURES["BLOCK_BLUE"].get_width():
self.load_textures()
temp = {}
for texture in self.TEXTURES.items():
temp[texture[0]] = scale(texture[1], (unitBlockPx, unitBlockPx))
self.TEXTURES = temp
class Blocks():
"""
Contains all section block definitions.
"""
"""
class block_demo():
def __init__(self):
self.TEXTURE :dict = {
"neutral": # Should be used as standard naming.
# NOTE: Can remove keys that are not in STATES, unlike shown.
{
"texture": [
Textures.TEXTURES["BLOCK_EXAMPLE_0"],
Textures.TEXTURES["BLOCK_EXAMPLE_1"]
],
"IS_ANIMATED": True, # Does this state has an animation, if so execute additional code to iterate through texture list.
"ANIMATION_REPEATS":-1, # Value of -1 indicates that animation runs infinitely.
# Can be left out if not IS_ANIMATED.
# Indicates the number of times the animation will loop in this state before passing to FOLLOW_UP_STATE.
"IS_INTERACTABLE": True, # Can be interacted with by the player during this state.
"IS_COLLIDABLE": False, # Player and entities will collide with block during this state.
"TIME_PER_FRAME": 1.0, # Amount of time (s) given to display each frame. Can be left out if not IS_ANIMATED.
"FOLLOW_UP_STATE": "on_trigger" # State that the block should progress to next. Can be left out if not IS_INTERACTABLE.
},
"on_trigger": # Should be used as standard naming.
{
"texture": [
Textures.TEXTURES["BLOCK_EXAMPLE"]
],
"IS_ANIMATED": False,
"IS_INTERACTABLE": False,
"IS_COLLIDABLE": False,
"TIME_PER_FRAME": 1.0, # TFP remains due to special temporary nature of on_trigger and off_trigger states.
"FOLLOW_UP_STATE": "while_trigger" # FUS remains due to special temporary nature of on_trigger and off_trigger states.
},
"while_trigger": # Should be used as standard naming.
{
"texture": [
Textures.TEXTURES["BLOCK_EXAMPLE"]
],
"IS_ANIMATED": False,
"IS_INTERACTABLE": True,
"IS_COLLIDABLE": False,
"TIME_PER_FRAME": 1.0,
"FOLLOW_UP_STATE": "off_trigger"
},
"off_trigger": # Should be used as standard naming.
{
"texture": [
Textures.TEXTURES["BLOCK_EXAMPLE"]
],
"IS_ANIMATED": False,
"IS_INTERACTABLE": False,
"IS_COLLIDABLE": False,
"TIME_PER_FRAME": 1.00,
"FOLLOW_UP_STATE": "neutral",
"ANIMATION_REPEATS":1
}
}
self.state :str = "neutral", # Variable for tracking the current texture.
self.textureIndex :int = "textureIndex": 0, # Tracks the index of the current animation frame.
self.animationClock :int = "animationClock": 0 # Used for tracking the time in frames, that a frame has been shown.
self.animationRepeats:int = 0 # Number of times that a limited animation has been run.
self.is_triggered :bool = False # Only needed if block in current state IS_INTERACTABLE
self.time :float = 0.0 # Used to track how long each frame has been displayed for animated textures.
self.layer :int = 0 # Used to track the layer of the block. For example, a door will be layer 1.
# while the floor below it is layer 0, so door gets interaction check priority.
def on_interact(self):
'''
Set of actions to execute upon interaction.
'''
"""
class block_blue():
def __init__(self, layer:int = 0):
self.TEXTURE = {
"neutral":{
"texture": [
Textures.TEXTURES["BLOCK_BLUE"]
],
"IS_ANIMATED": False,
"IS_INTERACTABLE": False,
"IS_COLLIDABLE": False
}
}
self.state = "neutral"
self.layer = layer
class block_rainbow():
def __init__(self, layer:int = 1):
self.TEXTURE = {
"neutral": {
"texture": [
Textures.TEXTURES["BLOCK_RED"],
Textures.TEXTURES["BLOCK_ORANGE"],
Textures.TEXTURES["BLOCK_YELLOW"],
Textures.TEXTURES["BLOCK_LIME"],
Textures.TEXTURES["BLOCK_GREEN"],
Textures.TEXTURES["BLOCK_CYAN"],
Textures.TEXTURES["BLOCK_LIGHT_BLUE"],
Textures.TEXTURES["BLOCK_BLUE"],
Textures.TEXTURES["BLOCK_PURPLE"],
Textures.TEXTURES["BLOCK_MAGENTA"],
Textures.TEXTURES["BLOCK_PINK"]
],
"IS_ANIMATED": True,
"ANIMATION_REPEATS": -1,
"IS_INTERACTABLE": True,
"IS_COLLIDABLE": True,
"TIME_PER_FRAME": 1.0,
"FOLLOW_UP_STATE": "on_trigger"
},
"on_trigger": {
"texture": [
Textures.TEXTURES["BLOCK_YELLOW"],
Textures.TEXTURES["BLOCK_ORANGE"],
Textures.TEXTURES["BLOCK_RED"]
],
"IS_ANIMATED": True,
"IS_INTERACTABLE": False,
"IS_COLLIDABLE": True,
"TIME_PER_FRAME": 1.0,
"ANIMATION_REPEATS": 5,
"FOLLOW_UP_STATE": "while_trigger"
},
"while_trigger": {
"texture": [
Textures.TEXTURES["BLOCK_PINK"],
Textures.TEXTURES["BLOCK_MAGENTA"],
Textures.TEXTURES["BLOCK_PURPLE"],
Textures.TEXTURES["BLOCK_BLUE"],
Textures.TEXTURES["BLOCK_LIGHT_BLUE"],
Textures.TEXTURES["BLOCK_CYAN"],
Textures.TEXTURES["BLOCK_GREEN"],
Textures.TEXTURES["BLOCK_LIME"],
Textures.TEXTURES["BLOCK_YELLOW"],
Textures.TEXTURES["BLOCK_ORANGE"],
Textures.TEXTURES["BLOCK_RED"]
],
"IS_ANIMATED": True,
"ANIMATION_REPEATS": -1,
"IS_INTERACTABLE": False,
"IS_COLLIDABLE": True,
"TIME_PER_FRAME": 1.0
}
}
self.state = "neutral"
self.textureIndex = 0
self.animationClock = 0
self.animationRepeats = 0
self.layer = layer
def on_interact(self):
"""
Set of actions to execute on interaction
"""
Section.update_state(section=self)
class puddle_of_souls():
def __init__(self, game, layer:int = 1):
self.TEXTURE = {
"neutral": {
"texture": [
Textures.TEXTURES["PUDDLE_SOULS_RICH"]
],
"IS_ANIMATED": False,
"IS_INTERACTABLE": True,
"IS_COLLIDABLE": True,
"FOLLOW_UP_STATE": "while_trigger"
},
"while_trigger": {
"texture": [
Textures.TEXTURES["PUDDLE_SOULS_SPENT"]
],
"IS_ANIMATED": False,
"IS_INTERACTABLE": False,
"IS_COLLIDABLE": False,
}
}
self.state = "neutral"
self.layer = layer
self.GAME = game
def on_interact(self):
"""
Set of actions to execute on interaction
"""
Section.update_state(section=self)
if randint(1, 100) <= 25:
self.GAME.PLAYER.health = self.GAME.PLAYER.health + 25
class puddle_of_blood():
def __init__(self, game, layer:int = 1):
self.TEXTURE = {
"on_death": {
"texture": [
Textures.TEXTURES["PUDDLE_BLOOD_0"],
Textures.TEXTURES["PUDDLE_BLOOD_1"],
Textures.TEXTURES["PUDDLE_BLOOD_2"],
Textures.TEXTURES["PUDDLE_BLOOD_3"],
Textures.TEXTURES["PUDDLE_BLOOD_4"]
],
"IS_ANIMATED": True,
"ANIMATION_REPEATS": 1,
"IS_INTERACTABLE": True,
"IS_COLLIDABLE": False,
"TIME_PER_FRAME": 0.25,
"FOLLOW_UP_STATE": "neutral"
},
"neutral": {
"texture": [
Textures.TEXTURES["PUDDLE_BLOOD_NEUTRAL"]
],
"IS_ANIMATED": False,
"IS_INTERACTABLE": True,
"IS_COLLIDABLE": False
}
}
self.state = "on_death"
self.textureIndex = 0
self.animationClock = 0
self.animationRepeats = 0
self.layer = layer
self.GAME = game
def on_interact(self):
"""
Set of actions to execute on interaction.
"""
self.GAME.PLAYER.effects.append(effect.War_Paint(self.GAME, self.GAME.PLAYER.character))

252
lib/system.py Executable file
View File

@@ -0,0 +1,252 @@
#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: System utilities.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: Utilities for a PyGame game.
#
# @Script: system.py
# @Date Created: 17 Apr, 2024
# @Last Modified: 14 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------
"""
Modules that are loaded will always be loaded globally.
Some will only be loaded based on conditions, but made global.
Thus, they can be checked for in globals() and be easily accessible anywhere.
Even if a module were loaded and unloaded, it would remain in cache, so unloading it is not really worth it.
Imports will be loaded as the program needs them, thus reducing the initial memory consumption.
"""
# Although they won't be reloaded and the imports will be skipped if called again,
# avoid wasting processing time by keeping out of import_dependencies()
from pkg_resources import working_set
from subprocess import check_call, CalledProcessError
from sys import executable
def install_dependencies(dependencies:dict):
"""
Tries to install and import any missing dependencies from list.
"""
# Adapted from Serge Stroobandt on https://stackoverflow.com/questions/46419607/how-to-automatically-install-required-packages-from-a-python-script-as-necessary
libsInst = {
pkg.key for pkg in working_set # Find required and installed libraries.
}
libsMiss = dependencies - libsInst # Remove the already installed libraries from the dependency list.
try:
if len(libsMiss) != 0:
check_call([executable, "-m", "pip", "install", "--upgrade", "pip"]) # Ensure pip is up-to-date.
for library in libsMiss:
check_call([executable, "-m", "pip", "install", library]) # Install all missing libraries.
except CalledProcessError:
print("Error | CANNOT FIND OR INSTALL DEPENDENCY:")
print(library)
raise SystemExit
install_dependencies({"icecream", "pygame"})
from icecream import ic
from pygame.version import vernum
from pygame.display import get_driver
from os import path
from sys import version as pyver
from platform import uname
from json import load as jload
DISPLAY = {}
PLATFORM = {"libraries": {}}
PLATFORM["kernel"] = uname().system # `system` is a variable of uname()
PLATFORM["libraries"]["pygame"] = vernum
# Python version cleanup.
versionStr = ""
for c in pyver:
if c == " ":
break
else:
versionStr = versionStr + c
versionTuple = tuple(versionStr.split("."))
versionTuple = (int(versionTuple[0]), int(versionTuple[1]), int(versionTuple[2]))
PLATFORM["libraries"]["python"] = versionTuple
# GCC version cleanup.
versionStr = ""
startFlag = False
for c in pyver:
if startFlag:
if c not in ["G", "C", "]"]:
versionStr = versionStr + c
elif c == "[":
startFlag = True
PLATFORM["libraries"]["GCC"] = versionStr
del pyver, versionStr, startFlag, versionTuple
# Fetch compatability settings.
DIRWORKING = path.dirname(__file__) + "/.."
with open(DIRWORKING + "/config/features.json") as f:
FEATURES = jload(f)
# All features in file default to is_active: false
# Enable supported features.
for feature in FEATURES.items():
is_active = False
for lib in feature[1]["libs"].items():
if PLATFORM["libraries"][lib[0]] > tuple(lib[1]["ver_implemented"]):
is_active = True
FEATURES[feature[0]]["is_active"] = is_active
class Compat():
def linux():
# Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate
"""
if "environ" not in globals(): # Import if not already.
global environ
from os import environ
try:
if environ["WAYLAND_DISPLAY"] != "" and globals["XDG_SESSION_TYPE"] != "x11": # Check for environment variable present on Wayland systems.
print("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.")
except KeyError: # Wayland keys won't exist on X11 systems (mostly).
install_dependencies({"Xlib"})
global display
from Xlib import display
global randr
from Xlib.ext import randr
displayGlobal = display.Display() # Get every display thing. Maybe a WM object?
default_screen = displayGlobal.get_default_screen() # Find default display for some reason.
info = displayGlobal.screen(default_screen) # Use this as a point to get information about all screens.
displayConfigs = randr.get_screen_resources(info.root) # Get every screen's possible configurations.
configsActive = set() # Create an empty set to add configurations to.
for config in displayConfigs.crtcs: # For every display configuration, determine if it's active and add its identifier to the configurations in use.
crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp)
if crtc_info.mode:
configsActive.add(crtc_info.mode)
for config in displayConfigs.modes:
if config.id in configsActive: # For every active display mode, figure out its framerate.
framerate = config.dot_clock / (config.h_total * config.v_total)
"""
if get_driver() == "wayland":
ic("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.")
elif get_driver() == "x11":
install_dependencies({"Xlib"})
global display
from Xlib import display
global randr
from Xlib.ext import randr
displayGlobal = display.Display() # Get every display thing. Maybe a WM object?
default_screen = displayGlobal.get_default_screen() # Find default display for some reason.
info = displayGlobal.screen(default_screen) # Use this as a point to get information about all screens.
displayConfigs = randr.get_screen_resources(info.root) # Get every screen's possible configurations.
configsActive = set() # Create an empty set to add configurations to.
for config in displayConfigs.crtcs: # For every display configuration, determine if it's active and add its identifier to the configurations in use.
crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp)
if crtc_info.mode:
configsActive.add(crtc_info.mode)
framerate = 30
# With multiple displays, get the highest framerate that any one monitor can do.
for config in displayConfigs.modes:
if config.id in configsActive: # For every active display mode, figure out its framerate.
framerateDisplay = (config.dot_clock / (config.h_total * config.v_total))
if framerateDisplay > framerate:
framerate = framerateDisplay
return framerate
def nt():
# Cannot use pygame.display.get_driver for windows or macos in pygame < v2
# Adapted from Anurag Uniyal on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate
install_dependencies({"pywin32"})
if "EnumDisplayDevices" not in globals(): # Import if not already.
global EnumDisplayDevices
from win32api import EnumDisplayDevices
if "EnumDisplaySettings" not in globals(): # Import if not already.
global EnumDisplaySettings
from win32api import EnumDisplaySettings
display_info = EnumDisplayDevices() # Returns PyDISPLAY_DEVICE object
display_settings = EnumDisplaySettings(display_info.DeviceName, -1) # Picks display that seems to have highest framerate?...
return getattr(display_settings, "DisplayFrequency")
def darwin():
# Cannot use pygame.display.get_driver for windows or macos in pygame < v2
# Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate
install_dependencies({"Appkit"})
if "NSScreen" not in globals(): # Import if not already.
global NSScreen
from AppKit import NSScreen
for screen in NSScreen.screens():
if framerate < screen.maximumFramesPerSecond(): # I the framerate of this display is higher, set its as the new maximum rate.
framerate = screen.maximumFramesPerSecond()
return framerate
def update_framerate():
"""
Used for mock vsync to support Pygame < v2.0.0
"""
if FEATURES["matchcase"]["is_active"]:
match PLATFORM["kernel"]:
case "Linux":
framerate = Compat.linux()
case "Windows":
framerate = Compat.nt()
case "Darwin":
framerate = Compat.darwin()
elif PLATFORM["kernel"] == "Linux":
framerate = Compat.linux()
elif PLATFORM["kernel"] == "Windows":
framerate = Compat.nt()
elif PLATFORM["kernel"] == "Darwin": # I'm trusting that the info I found for MacOS does actually still work this way.
framerate = Compat.darwin()
else:
print("Unsupported or unable to detect system kernel. Defaulting to 30 FPS. This can be overridden in the user settings file.")
framerate = int("%.0f" % framerate)
DISPLAY["framerate"] = framerate