def update(self, t=0, v=None): """ update render position (velocity is vector in OpenGL style coorinates/timestep)""" # if update() has not been run, set time_since_update to current time Sprite.update(self, t=t, v=v) p1, p2 = self.position_current if self.use_polar_coords: x, y = pol2cart(p1, p2) else: x, y = (p1, p2) sz = self.size th = self.thickness self.vertices = [ # horizontal beam (x - sz / 2.0, y + th / 2), # left-top (x - sz / 2.0, y - th / 2), # left-bottom (x + sz / 2.0, y - th / 2), # right-bottom (x + sz / 2.0, y + th / 2), # right-top # vertical beam (x - th / 2, y + sz / 2.0), # left-top (x - th / 2, y - sz / 2.0), # left-bottom (x + th / 2, y - sz / 2.0), # right-bottom (x + th / 2, y + sz / 2.0), # right-top ] self.t_since_update = t # set time_since_update to current time
class MainMenuLocation(_Location): def __init__(self, parent): super().__init__(parent) self.bg = Sprite( 0, 0, image=pygame.image.load("assets/misc/mainMenu.png").convert(), size=c.sizes["win"]) pygame.mixer.music.load("assets/audio/Intro.mp3") pygame.mixer.music.play(-1) self.play_button = Button( c.pads["play"], pygame.image.load("assets/misc/start.png").convert_alpha(), pygame.image.load( "assets/misc/startHighlight.png").convert_alpha()) self.exit = Button( c.pads["exit"], pygame.image.load("assets/misc/potQuit.png").convert(), pygame.image.load("assets/misc/potQuitHighlight.png").convert()) def update(self): mouse_pos = pygame.mouse.get_pos() self.bg.update(self.screen) self.play_button.update(self.screen, mouse_pos) self.exit.update(self.screen, mouse_pos) def event(self, event): mouse_pos = pygame.mouse.get_pos() if event.type == MOUSEBUTTONUP and event.button == 1: if self.play_button.click(mouse_pos): from .chooseYourPlants import LevelPreparationLocation self.parent.change_location( LevelPreparationLocation(self.parent)) if self.exit.click(mouse_pos): sys.exit()
class TopMenu: """ Class for handling menu on the top Which includes sun score indicator Plant card choices And shovel """ def __init__(self, cards: list = None, pos: int = 100): if cards is None: cards = [] # TODO добавить остальные цифры # Digits for sun display self.digits = { str(n): pygame.image.load(f"assets/misc/{n}.png").convert_alpha() for n in range(10) } # Main frame self.frame = Sprite( pos, 0, image=pygame.image.load("assets/misc/topmenu.png").convert_alpha(), size=c.sizes["topmenu"]) # Positions cards self.cards = pygame.sprite.Group() self.x = c.pads["sun"][0] + (c.pads["menubar"][0] if self.get_preparing() else 0) self.starting_x = self.x for card in cards: image = pygame.image.load( f"assets/cards/card{card.__name__}.png").convert() s = Card(self.x, card, image=image, size=c.sizes["card"]) self.cards.add(s) self.x += c.sizes["card"][0] + c.pads["cards"] self.shovel = Shovel(c.sizes["topmenu"][0] + c.pads["menubar"][0], 0) def update(self, screen, sun: int): """ Updates all parts of the interface :param screen: Surface :param sun: sun count int :return: None """ self.frame.update(screen) self.cards.update(screen, sun if self.get_preparing() else float("inf")) score_digits = list(map(lambda d: self.digits[d], str(sun))) digit_widths = list(map(lambda i: i.get_width(), score_digits)) offset = (c.pads["sun"][0] - sum(digit_widths)) // 2 + ( c.pads["menubar"][0] if self.get_preparing() else 0) for image, width in zip(score_digits, digit_widths): screen.blit(image, (offset, c.pads["sun"][1])) offset += width if self.get_preparing() and not self.shovel.taken: self.shovel.update(screen) def choose_card(self, mouse_pos: tuple, previous_choice: type or Shovel, suns: int) -> type or None: """ Checks mouse position on intersections with cards Changes choice or deletes it if clicked the same card :param mouse_pos: (x, y) :param previous_choice: plant type :param suns: int - checks if player can currently afford this plant :return: plant type or None """ if self.shovel.can_take(mouse_pos): if previous_choice.__class__.__name__ == "Shovel": previous_choice.put_back() return None return self.shovel.take() for card in self.cards: if card.rect.collidepoint(mouse_pos): choice = card.choose() # Last choice deletion if choice == previous_choice: return None if previous_choice.__class__.__name__ == "Shovel": previous_choice.put_back() if suns >= choice.sunCost: return choice return previous_choice def add_card(self, plant: type): """ Adds card during the preparation state :param plant: Plant :return: None """ if len(self.cards) > 5: return if any(filter(lambda card: card.plant == plant, self.cards)): return s = Card(self.x, image=pygame.image.load( f"assets/cards/card{plant.__name__}.png").convert(), size=c.sizes["card"], plant=plant) self.x += c.sizes["card"][0] + c.pads["cards"] self.cards.add(s) def remove_card(self, mouse_pos: tuple): """ Removes clicked card and moves other to the left :param mouse_pos: (x, y) :return: None """ for card in self.cards: if card.rect.collidepoint(mouse_pos): card.sound.play() self.cards.remove(card) # Reposition cards x = self.starting_x for card in self.cards: card.rect.x = x x += c.sizes["card"][0] + c.pads["cards"] self.x = x return def move_right(self): """ After removing card all cards need to be moved left To remove gap :return: None """ dx = c.pads["menubar"][0] self.x += dx for card in self.cards: card.rect.x += dx self.frame.rect.x += dx def lock_card(self, plant: type): """ After planting player have to wait until he can use plant again :param plant: Plant :return: None """ for card in self.cards: if card.plant == plant: card.set_timer() def get_preparing(self) -> bool: """ Check where widget if located :return: bool """ return self.frame.rect.x == 100
class PlantChoiceMenu: def __init__(self, cards): self.frame = Sprite( 0, c.sizes["topmenu"][1], image=pygame.image.load( "assets/misc/chooseYourPlants.png").convert_alpha(), size=c.sizes["choose"]) self.button_images = list( map(lambda i: pygame.transform.smoothscale(i, c.sizes["letsRock"]), [ pygame.image.load( "assets/misc/letsRock.png").convert_alpha(), pygame.image.load( "assets/misc/letsRockHighlight.png").convert_alpha() ])) self.button = Sprite(158, 568, image=self.button_images[0]) self.cards = pygame.sprite.Group() self.x = c.pads["choose"][0] self.starting_x = self.x y = c.pads["choose"][1] _c = 0 for card in cards: _c += 1 image = pygame.image.load( f"assets/cards/card{card.__name__}.png").convert() s = Card(self.x, card, image=image, size=c.sizes["card"], y=y) self.cards.add(s) self.x += c.sizes["card"][0] + 2 if _c % 7 == 0: self.x = self.starting_x y += c.sizes["card"][1] + 5 def update(self, screen, mouse_pos): """ :param screen: Surface :param mouse_pos: (x, y) :return: """ # Marks the Let's Rock button if mouse collides with it if self.button.rect.collidepoint(mouse_pos): self.button.image = self.button_images[1] else: self.button.image = self.button_images[0] self.frame.update(screen) self.button.update(screen) self.cards.update(screen, float("inf")) def choose_card(self, mouse_pos: tuple) -> type: """ Checks mouse position on intersections with cards Returns choice or None :param mouse_pos: (x, y) :return: plant type or None """ for card in self.cards: if card.rect.collidepoint(mouse_pos): choice = card.choose() return choice def button_click(self, mouse_pos: tuple) -> bool: return self.button.rect.collidepoint(mouse_pos)
class GameLocation(_Location): def __init__(self, parent, top_menu): super().__init__(parent) # Game interface self.plant_choice = self.plant_choice_image = None self.suns = c.starting_sun + 10000 # Sprite groups for different game objects self.suns_group = pygame.sprite.Group() self.zombies = pygame.sprite.Group() self.projectiles = pygame.sprite.Group() self.lawnmovers = [LawnMower(row) for row in range(5)] # Background is a sprite for easier drawing self.background = Sprite( 0, 0, image=pygame.image.load("assets/misc/sm_bg.png").convert_alpha(), size=c.sizes["win"]) # Menu with plant choices, shovel and suns count indication self.menubar = top_menu # Creating game field self.cells = [] for y in range(c.YCells): grid_row = [] for x in range(c.XCells): tile = Grass(x, y) grid_row.append(tile) self.cells.append(grid_row) # Adds event of falling sun for every sun_drop_delay seconds pygame.time.set_timer(USEREVENT + 1, c.sun_drop_delay * 1000) # Music pygame.mixer_music.load("assets/audio/grasswalk.mp3") pygame.mixer.music.set_volume(0.75) pygame.mixer_music.play(loops=-1) self.zombies.add(NormalZombie(0)) self.zombies.add(BucketHeadZombie(1)) def update(self): self.background.update(self.screen) self.menubar.update(self.screen, self.suns) x, y = pygame.mouse.get_pos() # Draws from right to left for row in self.cells: for cell in reversed(row): cell.update(self.screen) # Transparent image of chosen plant if cell.isempty() and self.plant_choice is not None \ and cell.rect.collidepoint((x, y)) and \ self.plant_choice.__class__.__name__ != "Shovel": # In the middle of the cell self.screen.blit( transform_image(self.plant_choice_image), cell.get_middle_pos(self.plant_choice_image)) self.lawnmowers_update() # Start drawing from right top for zombie in sorted(self.zombies.sprites(), key=lambda z: z.row * c.XCells + z.col): zombie.update(self.screen) self.zombie_targeting() # Draw choice near the mouse if self.plant_choice is not None: self.screen.blit(self.plant_choice_image, (x - self.plant_choice_image.get_width() / 2, y - self.plant_choice_image.get_height())) self.projectiles.update(self.screen) self.projectile_collisions_check() self.suns_group.update(self.screen) def event(self, event): """ Responds to game events :param event: pygame.event :return: None """ # Drop sun if event.type == USEREVENT + 1: self.drop_sun() elif event.type == MOUSEBUTTONUP and event.button == 1: x, y = pygame.mouse.get_pos() # Check order: # 1. Player clicked on a sun # 2. Player has chosen anything on the top menu # 3. Player planted plant / removed one using shovel # Suns check for sun in self.suns_group: if sun.check_collision((x, y)): sun.fly() self.suns += 25 return # Top menu choice check choice = self.menubar.choose_card((x, y), self.plant_choice, self.suns) if choice != self.plant_choice: self.plant_choice = choice self.plant_choice_image = None if self.plant_choice is not None: self.plant_choice_image = self.plant_choice.get_shadow() return # Plant / use shovel # If player's suns count is lesser than the needed for this plant if self.plant_choice.__class__.__name__ != "Shovel": if self.plant_choice is None or self.suns < self.plant_choice.sunCost: return x -= c.pads["game"][0] y -= c.pads["game"][1] if x < 0 or y < 0: if self.plant_choice.__class__.__name__ == "Shovel": self.plant_choice.put_back() self.plant_choice = self.plant_choice_image = None return try: cell = self.cells[y // c.sizes["cell"][1]][x // c.sizes["cell"][0]] except IndexError: self.plant_choice = self.plant_choice_image = None return if self.plant_choice.__class__.__name__ == "Shovel": # If player is using shovel remove plant if cell.remove_plant(): self.suns += cell.sun self.plant_choice.put_back() self.plant_choice = self.plant_choice_image = None elif cell.plant(self.plant_choice, self.suns_group, self.projectiles): # Else plant self.menubar.lock_card(self.plant_choice) self.suns -= self.plant_choice.sunCost self.plant_choice = self.plant_choice_image = None def drop_sun(self): """ This method is called periodically through custom pygame event Drops sun in random place on random height """ max_y = random.randint(c.sizes["win"][1] // 3, c.sizes["win"][1] * 2 // 3) x = random.randint(c.sizes["win"][0] // 10, c.sizes["win"][0] * 9 // 10) self.suns_group.add(Sun(x, c.sizes["topmenu"][1], max_y)) def projectile_collisions_check(self): """ Checks all projectiles on collisions with zombies Calls zombie method take_damage if so """ for zombie in self.zombies: # Checks for hits for projectile in filter( lambda p: p.row == zombie.row and p.rect.x >= zombie.rect. x, self.projectiles): zombie.take_damage(projectile) # Checks for potato mine explosions for cell in filter(lambda ce: ce.col == zombie.col, self.cells[zombie.row]): plant = cell.planted.sprite if plant.__class__.__name__ == "PotatoMine": if plant.armed: plant.explode(zombie) # Check if chompers can eat zombie for cell in self.cells[zombie.row][zombie.col - 1:zombie.col + 1]: plant = cell.planted.sprite if plant.__class__.__name__ == "Chomper": if not plant.busy(): plant.catch(zombie) # Check for cherryBomb explosion for row in self.cells: for cell in row: plant = cell.planted.sprite if plant.__class__.__name__ == "CherryBomb": if plant.armed and plant.health > 0: row, col = plant.coords plant.explode(*filter( lambda z: z.row in [row - 1, row, row + 1] and z. col in [col - 1, col, col + 1], self.zombies)) def lawnmowers_update(self): """ Checks for zombie crossing the last cell If row lawnmover exists, make it run Else game is lost """ for zombie in self.zombies: if zombie.col < 0: lawnmower = self.lawnmovers[zombie.row] if lawnmower is None: self.lose() return lawnmower.run() for ind, lawnmower in enumerate(self.lawnmovers): if lawnmower is None: continue if lawnmower.update(self.screen): self.lawnmovers[ind] = None continue if not lawnmower.running: continue for zombie in self.zombies: if zombie.row == lawnmower.row and \ lawnmower.rect.x > zombie.rect.x: zombie.kill() def zombie_targeting(self): """ Checks for cell intersections between plants and zombies Makes zombie eat plant if so """ for zombie in filter(lambda z: not z.busy(), self.zombies): for cell in filter( lambda ce: not ce.isempty() and ce.col == zombie.col, self.cells[zombie.row]): plant = cell.planted zombie.change_target(plant) def lose(self): pass
class LevelPreparationLocation(_Location): def __init__(self, parent): super().__init__(parent) # Location music pygame.mixer_music.load("assets/audio/chooseYourSeeds.mp3") pygame.mixer_music.play(loops=-1) self.bg = Sprite( 0, 0, image=pygame.image.load("assets/misc/bg.png").convert()) # At the begging camera moves to the right side of background self.move_times = (self.bg.image.get_width() - c.sizes["win"][0]) / 5 # Card choices self.plant_choice_widget = PlantChoiceMenu( # Contains all working plants [ PotatoMine, Sunflower, SnowPea, CherryBomb, Chomper, Repeater, WallNut, PeaShooter ]) # Menu which usually displays suns and chosen cards # New cards added here self.top_menu = TopMenu(pos=0) # Sounds and images for pre-game state self.ready_set_plant = pygame.mixer.Sound( "assets/audio/readysetplant.wav") self.ready_set_plant_images = [ pygame.image.load("assets/misc/StartReady.png").convert_alpha(), pygame.image.load("assets/misc/StartSet.png").convert_alpha(), pygame.image.load("assets/misc/StartPlant.png").convert_alpha(), ] self.counter = 0 def update(self): if self.move_times > 0: # Moves right before seeds choice self.bg.rect.x -= 5 self.move_times -= 1 elif self.move_times < 0: # Moves left after seeds choice self.bg.rect.x += 5 self.move_times += 1 if self.move_times == 0: self.ready_set_plant.play() self.counter = 1 self.bg.update(self.screen) if not self.move_times and not self.counter: self.plant_choice_widget.update(self.screen, pygame.mouse.get_pos()) self.top_menu.update(self.screen, c.starting_sun) elif self.counter: self.counter += 1 image = None if self.counter >= c.time_afterRSP: # Change game location on game self.top_menu.move_right() self.parent.change_location(GameLocation(self, self.top_menu)) # Ready Set Plant messages for i, time in enumerate(c.time_readySetPlant): if self.counter >= time: image = self.ready_set_plant_images[2 - i] break if image is not None: self.screen.blit( image, ((c.sizes["win"][0] - image.get_width()) / 2, (c.sizes["win"][1] - image.get_height()) / 2)) def event(self, event): if event.type == MOUSEBUTTONUP and event.button == 1: mouse_pos = pygame.mouse.get_pos() self.top_menu.remove_card(mouse_pos) choice = self.plant_choice_widget.choose_card(mouse_pos) if choice is not None: self.top_menu.add_card(choice) if self.plant_choice_widget.button_click(mouse_pos): self.move_times = -(self.bg.image.get_width() - c.sizes["win"][0] - c.pads["game"][0]) // 5