#!~/.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))