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