This repository has been archived on 2025-07-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
shroom_of_doom/lib/entity.py
2025-02-26 14:40:25 -05:00

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)