Initial Commit
This commit is contained in:
617
lib/entity.py
Normal file
617
lib/entity.py
Normal 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)
|
||||
Reference in New Issue
Block a user