617 lines
22 KiB
Python
617 lines
22 KiB
Python
#!~/.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) |