mirror of
				https://git.davidovski.xyz/tkinter-space-game.git
				synced 2024-08-15 00:43:41 +00:00 
			
		
		
		
	Initial Commit
This commit is contained in:
		
						commit
						b99dbb396c
					
				
					 19 changed files with 3912 additions and 0 deletions
				
			
		
							
								
								
									
										203
									
								
								boss.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								boss.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,203 @@ | ||||||
|  | from dataclasses import replace | ||||||
|  | import math | ||||||
|  | 
 | ||||||
|  | from enemy import EnemyAttributes | ||||||
|  | from formation import ( | ||||||
|  |     CircleFormation, | ||||||
|  |     CircleFormationAttributes, | ||||||
|  |     EnemyFormation, | ||||||
|  |     FormationAttributes, | ||||||
|  |     FormationEnemy, | ||||||
|  | ) | ||||||
|  | from game import Game | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CircleBossFormation(EnemyFormation): | ||||||
|  |     """Enemy Formation for the circular game boss""" | ||||||
|  | 
 | ||||||
|  |     RADIUS = 10 | ||||||
|  |     CYCLE_PEROID = 500 | ||||||
|  |     COUNT = 6 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, attributes: EnemyAttributes): | ||||||
|  |         """Initialise the circle boss | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this boss belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param attributes: The attributes on which to base spawned enemies | ||||||
|  |         :type attributes: EnemyAttributes | ||||||
|  |         """ | ||||||
|  |         self.image_name = "enemy0" | ||||||
|  |         self.minion_image_name = "smallenemy0" | ||||||
|  | 
 | ||||||
|  |         self.alpha = 0 | ||||||
|  | 
 | ||||||
|  |         attributes = CircleFormationAttributes( | ||||||
|  |             radius=40, | ||||||
|  |             period=300, | ||||||
|  |             count=CircleBossFormation.COUNT, | ||||||
|  |             velocity=attributes.velocity, | ||||||
|  |             cooldown=attributes.cooldown, | ||||||
|  |             hp=attributes.hp//2, | ||||||
|  |             reward=attributes.reward*10 | ||||||
|  |         ) | ||||||
|  |         self.circle_formation = CircleFormation( | ||||||
|  |             game, self.minion_image_name, attributes) | ||||||
|  | 
 | ||||||
|  |         super().__init__(game, self.image_name, attributes) | ||||||
|  | 
 | ||||||
|  |     def create_enemies(self): | ||||||
|  |         """Spawn the boss""" | ||||||
|  |         self.spawn_enemy((-4, -4, 0)) | ||||||
|  | 
 | ||||||
|  |     def tick(self, player): | ||||||
|  |         """Update the boss's position | ||||||
|  | 
 | ||||||
|  |         :param player: The player which to check collision with | ||||||
|  |         :type player: Player | ||||||
|  |         """ | ||||||
|  |         super().tick(player) | ||||||
|  |         self.circle_formation.x = self.x | ||||||
|  |         self.circle_formation.y = self.y | ||||||
|  |         a = (self.alpha/CircleBossFormation.CYCLE_PEROID)*2*math.pi - math.pi | ||||||
|  | 
 | ||||||
|  |         r = 50*math.sin(a) - 25 | ||||||
|  | 
 | ||||||
|  |         p = math.sin(a*2)*100 | ||||||
|  | 
 | ||||||
|  |         self.circle_formation.attributes.radius = math.floor( | ||||||
|  |             CircleBossFormation.RADIUS + (r if r > 0 else 0) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         self.circle_formation.attributes.period = math.floor( | ||||||
|  |             400 + (p if p < 100 else 100) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         self.circle_formation.tick(player) | ||||||
|  | 
 | ||||||
|  |         # When the boss is dead, the minions will all die | ||||||
|  |         if len(self.sprites) == 0: | ||||||
|  |             if len(self.circle_formation.sprites) > 0: | ||||||
|  |                 self.circle_formation.sprites[0].damage() | ||||||
|  |             else: | ||||||
|  |                 self.destroy() | ||||||
|  | 
 | ||||||
|  |     def destroy(self): | ||||||
|  |         """Remove the circle boss""" | ||||||
|  |         super().destroy() | ||||||
|  |         self.circle_formation.destroy() | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Hide the circle boss""" | ||||||
|  |         self.circle_formation.hide() | ||||||
|  |         return super().hide() | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Show the circle boss""" | ||||||
|  |         self.circle_formation.show() | ||||||
|  |         return super().show() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SnakeBossFormation(EnemyFormation): | ||||||
|  |     """Enemy formation for the snake boss""" | ||||||
|  | 
 | ||||||
|  |     LENGTH = 32 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, attributes: FormationAttributes): | ||||||
|  |         """Initialise the snake boss | ||||||
|  | 
 | ||||||
|  |         :param game: The game which the boss belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param attributes: The attributes of which to base spawned enemies on | ||||||
|  |         :type attributes: FormationAttributes | ||||||
|  |         """ | ||||||
|  |         self.minion_name = "smallenemy1" | ||||||
|  |         self.tail_name = "smallenemy1_evil" | ||||||
|  |         self.head_name = "enemy2" | ||||||
|  | 
 | ||||||
|  |         self.phase = 1 | ||||||
|  |         self.phase_timer = 0 | ||||||
|  | 
 | ||||||
|  |         super().__init__(game, self.minion_name, attributes) | ||||||
|  | 
 | ||||||
|  |     def create_enemies(self): | ||||||
|  |         """Spawn the snake""" | ||||||
|  |         head_attributes = replace(self.attributes) | ||||||
|  |         head_attributes.hp *= 100 | ||||||
|  |         self.head = FormationEnemy(self.game, self.head_name, | ||||||
|  |                                    (0, 0, 0), head_attributes) | ||||||
|  | 
 | ||||||
|  |         self.sprites.append(self.head) | ||||||
|  | 
 | ||||||
|  |         for i in range(SnakeBossFormation.LENGTH): | ||||||
|  |             self.spawn_enemy((0, 0, i+1)) | ||||||
|  | 
 | ||||||
|  |         tail_attributes = replace(self.attributes) | ||||||
|  |         head_attributes.hp //= 5 | ||||||
|  |         self.tail = FormationEnemy(self.game, self.tail_name, | ||||||
|  |                                    (0, 0, SnakeBossFormation.LENGTH+1), | ||||||
|  |                                    tail_attributes) | ||||||
|  | 
 | ||||||
|  |         self.sprites.append(self.tail) | ||||||
|  | 
 | ||||||
|  |     def spawn_enemy(self, offset): | ||||||
|  |         """Spawn one enemy unit of the snake | ||||||
|  | 
 | ||||||
|  |         :param offset: The offset of the enemy | ||||||
|  |         """ | ||||||
|  |         attributes = replace(self.attributes) | ||||||
|  |         if offset[2] % 6 == 0: | ||||||
|  |             attributes.cooldown = 40 | ||||||
|  |         else: | ||||||
|  |             attributes.cooldown = -1 | ||||||
|  | 
 | ||||||
|  |         enemy = FormationEnemy(self.game, self.image_name, offset, attributes) | ||||||
|  |         self.sprites.append(enemy) | ||||||
|  |         return enemy | ||||||
|  | 
 | ||||||
|  |     def position_enemy(self, enemy: FormationEnemy): | ||||||
|  |         """Position the enemy on the game screen | ||||||
|  | 
 | ||||||
|  |         :param enemy: The enemy to position | ||||||
|  |         :type enemy: FormationEnemy | ||||||
|  |         """ | ||||||
|  |         if self.phase == 2: | ||||||
|  |             p = 120 / (100 + math.cos(self.phase_timer / 400)*20) * 120 | ||||||
|  |         else: | ||||||
|  |             p = 120 | ||||||
|  | 
 | ||||||
|  |         m = 4 | ||||||
|  |         t = ((-enemy.offset_a*m) + self.game.alpha) / p + math.pi | ||||||
|  |         a = self.game.w // 2 | ||||||
|  |         b = self.game.h // 3 | ||||||
|  |         c = 0 | ||||||
|  | 
 | ||||||
|  |         if self.phase == 2: | ||||||
|  |             n = 10 - (2000 / (self.phase_timer+2000))*5 | ||||||
|  |         else: | ||||||
|  |             n = 5 | ||||||
|  | 
 | ||||||
|  |         enemy.set_pos(( | ||||||
|  |             int(self.x + a*math.sin(n*t+c)), | ||||||
|  |             int(self.y + b*math.sin(t)) | ||||||
|  |         )) | ||||||
|  | 
 | ||||||
|  |     def tick(self, player): | ||||||
|  |         """Update the position of the enemies | ||||||
|  | 
 | ||||||
|  |         :param player: The player which to check collision with | ||||||
|  |         """ | ||||||
|  |         super().tick(player) | ||||||
|  | 
 | ||||||
|  |         if self.phase == 1: | ||||||
|  |             self.head.hp = self.attributes.hp*100 | ||||||
|  |             if self.tail.destroyed: | ||||||
|  |                 if len(self.sprites) > 1: | ||||||
|  |                     self.sprites[-1].damage(amount=(self.attributes.hp//4)) | ||||||
|  |                 else: | ||||||
|  |                     self.head.hp = self.attributes.hp * 3 | ||||||
|  |                     self.phase = 2 | ||||||
|  |         elif self.phase == 2: | ||||||
|  |             self.phase_timer += 1 | ||||||
|  |             self.head.attributes.cooldown = int( | ||||||
|  |                 20 + math.sin(self.phase_timer / 50)*10) | ||||||
							
								
								
									
										127
									
								
								boss_key.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								boss_key.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,127 @@ | ||||||
|  | from config import Config | ||||||
|  | from game import Game | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BossKey(): | ||||||
|  |     """Object which manages the 'boss key' feature | ||||||
|  |        When a key is pressed, then the screen switches to a "work" | ||||||
|  |        related image | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     FG = "#ffaa00" | ||||||
|  |     BG = "#aaaaaa" | ||||||
|  |     BG2 = "#ffffff" | ||||||
|  |     FG2 = "#555555" | ||||||
|  |     TEXT_SIZE = 30 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, pause_callback) -> None: | ||||||
|  |         """Initialises the boss key feature | ||||||
|  | 
 | ||||||
|  |         :param game: The game which to use | ||||||
|  |         :type game: Game | ||||||
|  |         :param pause_callback: The function to call to pause the game | ||||||
|  |         :rtype: None | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.canvas = game.canvas | ||||||
|  |         self.width, self.height = game.w * Config.SCALE, game.h * Config.SCALE | ||||||
|  |         self.shapes = [] | ||||||
|  |         self.game.inputs.add_keypress_handler(self.on_key) | ||||||
|  |         self.hidden = True | ||||||
|  |         self.pause_callback = pause_callback | ||||||
|  | 
 | ||||||
|  |     def on_key(self, event): | ||||||
|  |         """Handle key press events | ||||||
|  | 
 | ||||||
|  |         :param event: The key press event | ||||||
|  |         """ | ||||||
|  |         if event.keysym == self.game.inputs.settings.boss \ | ||||||
|  |                 and self.hidden: | ||||||
|  |             self.pause_callback() | ||||||
|  |             self.create_shapes() | ||||||
|  |             self.hidden = False | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  |         if not self.hidden: | ||||||
|  |             self.delete_shapes() | ||||||
|  |             self.hidden = True | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def create_rectangle(self, x, y, w, h, color): | ||||||
|  |         """Create a rectangle object | ||||||
|  | 
 | ||||||
|  |         :param x: x coordinate | ||||||
|  |         :param y: y coordinate | ||||||
|  |         :param w: width | ||||||
|  |         :param h: height | ||||||
|  |         :param color: The colour of the rectangle | ||||||
|  |         """ | ||||||
|  |         self.shapes.append(self.canvas.create_rectangle( | ||||||
|  |             x, y, x+w, y+h, fill=color, state="disabled")) | ||||||
|  | 
 | ||||||
|  |     def write_text(self, x, y, text): | ||||||
|  |         """Create a text object | ||||||
|  | 
 | ||||||
|  |         :param x: x coordiante | ||||||
|  |         :param y: y coordinate | ||||||
|  |         :param text: The text used for this label | ||||||
|  |         """ | ||||||
|  |         self.shapes.append(self.canvas.create_text( | ||||||
|  |             x, y, text=text, fill=BossKey.BG2, | ||||||
|  |             font=(f"Helvetica {BossKey.TEXT_SIZE} bold"), state="disabled")) | ||||||
|  | 
 | ||||||
|  |     def create_shapes(self): | ||||||
|  |         """Create all the shapes needed for the calculator""" | ||||||
|  |         width = self.width | ||||||
|  |         height = self.height | ||||||
|  |         padding = width // 50 | ||||||
|  | 
 | ||||||
|  |         num_rows = 5 | ||||||
|  |         num_cols = 4 | ||||||
|  | 
 | ||||||
|  |         grid_width = width // num_cols | ||||||
|  |         grid_height = height // (num_rows+1) | ||||||
|  | 
 | ||||||
|  |         self.create_rectangle(0, 0, width, height, BossKey.BG) | ||||||
|  |         self.create_rectangle(padding, | ||||||
|  |                               padding, | ||||||
|  |                               width - padding*2, | ||||||
|  |                               grid_height-padding*2, | ||||||
|  |                               BossKey.FG2) | ||||||
|  | 
 | ||||||
|  |         symbols = [ | ||||||
|  |             "(", ")", "%", "AC", | ||||||
|  |             "7", "8", "9", "/", | ||||||
|  |             "4", "5", "6", "x", | ||||||
|  |             "1", "2", "3", "-", | ||||||
|  |             "0", ".", "=", "+" | ||||||
|  |         ] | ||||||
|  |         for row in range(num_rows): | ||||||
|  |             for col in range(num_cols): | ||||||
|  |                 color = BossKey.FG2 | ||||||
|  |                 if row == 0 or col == num_cols - 1: | ||||||
|  |                     color = BossKey.FG | ||||||
|  |                 x = col*grid_width+padding | ||||||
|  |                 y = row*grid_height+padding+grid_height | ||||||
|  |                 w = grid_width-padding*2 | ||||||
|  |                 h = grid_height-padding*2 | ||||||
|  |                 self.create_rectangle(x, y, w, h, color) | ||||||
|  | 
 | ||||||
|  |                 offset_x = x + padding + ( | ||||||
|  |                     grid_width | ||||||
|  |                     - padding*2 | ||||||
|  |                     - BossKey.TEXT_SIZE) // 2 | ||||||
|  | 
 | ||||||
|  |                 offset_y = y + padding + ( | ||||||
|  |                     grid_height-padding * 2 | ||||||
|  |                     - BossKey.TEXT_SIZE) // 2 | ||||||
|  | 
 | ||||||
|  |                 symbol = symbols[col + row*num_cols] | ||||||
|  |                 self.write_text(offset_x, offset_y, symbol) | ||||||
|  | 
 | ||||||
|  |     def delete_shapes(self): | ||||||
|  |         """Remove all the shapes used for the calculator""" | ||||||
|  |         for shape in self.shapes: | ||||||
|  |             self.canvas.delete(shape) | ||||||
							
								
								
									
										162
									
								
								cheat_engine.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								cheat_engine.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,162 @@ | ||||||
|  | from typing import Callable, List | ||||||
|  | 
 | ||||||
|  | from config import Config | ||||||
|  | from menu import Menu | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Cheat: | ||||||
|  |     """A single cheat, to be assigned to a cheat engine""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game, code: List[str], callback: Callable): | ||||||
|  |         """Initialise the cheat | ||||||
|  | 
 | ||||||
|  |         :param game: The game which the cheat belongs to | ||||||
|  |         :param code: A string of key codes to be pressed to activate the cheat | ||||||
|  |         :type code: List[str] | ||||||
|  |         :param callback: The function to be called when this cheat is activated | ||||||
|  |         :type callback: Callable | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.code = code | ||||||
|  |         self.callback = callback | ||||||
|  |         self.position = 0 | ||||||
|  | 
 | ||||||
|  |     def on_key(self, event): | ||||||
|  |         """Handle a key press event | ||||||
|  | 
 | ||||||
|  |         :param event: The event that is being handled | ||||||
|  |         """ | ||||||
|  |         if self.position < len(self.code): | ||||||
|  |             next_key = self.code[self.position] | ||||||
|  |             if event.keysym == next_key: | ||||||
|  |                 self.position += 1 | ||||||
|  |                 if self.position == len(self.code): | ||||||
|  |                     self.callback() | ||||||
|  |                     self.position = 0 | ||||||
|  |             else: | ||||||
|  |                 self.position = 0 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class InvincibilityCheat(Cheat): | ||||||
|  |     """Cheat that makes the player invincible""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game, code: List[str]): | ||||||
|  |         """Initialise the cheat | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :param code: The combination of characters for which to | ||||||
|  |                      activate this cheat | ||||||
|  |         :type code: List[str] | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, code, self.toggle) | ||||||
|  |         self.enabled = False | ||||||
|  |         self.damage_function = None | ||||||
|  | 
 | ||||||
|  |     def toggle(self): | ||||||
|  |         """Enable or disable this cheat""" | ||||||
|  |         self.enabled = not self.enabled | ||||||
|  |         if self.enabled: | ||||||
|  |             self.game.effect_player.splash_text("Godmode on") | ||||||
|  |             self.game.player.set_image(self.game.player.white_image) | ||||||
|  |             self.damage_function = self.game.player.damage | ||||||
|  |             self.game.player.damage = (lambda: None) | ||||||
|  |         else: | ||||||
|  |             self.game.effect_player.splash_text("Godmode off") | ||||||
|  |             self.game.player.set_image( | ||||||
|  |                 self.game.texture_factory.get_image("ship")) | ||||||
|  |             self.game.player.damage = self.damage_function | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DevModeCheat(Cheat): | ||||||
|  |     """Cheat that enables 'dev mode' which: | ||||||
|  |        - enables spawning menu | ||||||
|  |        - key to remove all enemies | ||||||
|  |        - key to stop spawning outright | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game, code: List[str]): | ||||||
|  |         """Initialise the cheat | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :param code: The combination of characters which activate this cheat | ||||||
|  |         :type code: List[str] | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, code, self.toggle) | ||||||
|  |         self.enabled = Config.DEVMODE | ||||||
|  | 
 | ||||||
|  |         self.spawning_disabled = False | ||||||
|  |         self.spawn_menu = Menu(self.game, "Spawn Menu") | ||||||
|  |         for i in ("circle_boss", | ||||||
|  |                   "snake_boss", | ||||||
|  |                   "loop", | ||||||
|  |                   "orbital", | ||||||
|  |                   "rectangle", | ||||||
|  |                   "fleet"): | ||||||
|  |             self.spawn_menu.add_item(i, self.spawn_item(i)) | ||||||
|  | 
 | ||||||
|  |     def toggle(self): | ||||||
|  |         """Toggle if this mode is enabled""" | ||||||
|  |         self.enabled = not self.enabled | ||||||
|  |         if self.enabled: | ||||||
|  |             self.game.effect_player.splash_text("devmode on") | ||||||
|  |         else: | ||||||
|  |             self.game.effect_player.splash_text("devmode off") | ||||||
|  | 
 | ||||||
|  |     def spawn_item(self, name): | ||||||
|  |         """Spawn a named item from the menu | ||||||
|  | 
 | ||||||
|  |         :param name: The name of the formation to spawn | ||||||
|  |         """ | ||||||
|  |         return lambda: ( | ||||||
|  |             self.spawn_menu.hide(), | ||||||
|  |             getattr(self.game.formation_spawner, f"spawn_{name}")() | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def on_key(self, event): | ||||||
|  |         """Handle Key press events | ||||||
|  | 
 | ||||||
|  |         :param event: The key press event to handle | ||||||
|  |         """ | ||||||
|  |         if self.enabled: | ||||||
|  |             if event.keysym == "n": | ||||||
|  |                 self.game.formation_spawner.clear_all() | ||||||
|  |                 self.game.formation_spawner.next_phase() | ||||||
|  | 
 | ||||||
|  |             if event.keysym == "c": | ||||||
|  |                 self.game.formation_spawner.clear_all() | ||||||
|  | 
 | ||||||
|  |             if event.keysym == "k": | ||||||
|  |                 if self.spawning_disabled: | ||||||
|  |                     self.game.effect_player.splash_text("spawning on") | ||||||
|  |                     self.spawning_disabled = False | ||||||
|  |                     self.game.formation_spawner.next_formation = 0 | ||||||
|  |                 else: | ||||||
|  |                     self.game.effect_player.splash_text("spawning off") | ||||||
|  |                     self.game.formation_spawner.clear_all() | ||||||
|  |                     self.game.formation_spawner.next_formation = -1 | ||||||
|  | 
 | ||||||
|  |             if event.keysym == "m": | ||||||
|  |                 self.spawn_menu.show() | ||||||
|  | 
 | ||||||
|  |         return super().on_key(event) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CheatEngine: | ||||||
|  |     """Object which manages cheats""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game): | ||||||
|  |         """Initialise the cheat engine | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.cheats = [] | ||||||
|  | 
 | ||||||
|  |     def add_cheat(self, cheat): | ||||||
|  |         """Register a cheat to the engine | ||||||
|  | 
 | ||||||
|  |         :param cheat: The cheat to be registered | ||||||
|  |         """ | ||||||
|  |         self.game.inputs.add_keypress_handler(cheat.on_key) | ||||||
|  |         self.cheats.append(cheat) | ||||||
							
								
								
									
										20
									
								
								config.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								config.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | from dataclasses import dataclass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class Config: | ||||||
|  |     """Various constants and configuration for the game""" | ||||||
|  | 
 | ||||||
|  |     WIDTH = 100 | ||||||
|  |     HEIGHT = 200 | ||||||
|  |     # Number of window pixels used for each game "pixel" | ||||||
|  |     SCALE = 6 | ||||||
|  | 
 | ||||||
|  |     FPS = 30 | ||||||
|  | 
 | ||||||
|  |     NICK_LEN = 3 | ||||||
|  |     DEVMODE = False | ||||||
|  | 
 | ||||||
|  |     LEADERBOARD_FILE = "leaderboard" | ||||||
|  |     SAVE_FILE = "save" | ||||||
|  |     SETTINGS_FILE = "settings" | ||||||
							
								
								
									
										63
									
								
								enemy.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								enemy.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | from dataclasses import dataclass | ||||||
|  | 
 | ||||||
|  | from game import Game | ||||||
|  | from shooter import Shooter, ShooterAttributes | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class EnemyAttributes(ShooterAttributes): | ||||||
|  |     """Attributes of an enemy object""" | ||||||
|  | 
 | ||||||
|  |     reward: int = 100 | ||||||
|  |     lazer_color: str = "red" | ||||||
|  |     cooldown: int = 20 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Enemy(Shooter): | ||||||
|  |     """An enemy in the game""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name: str, | ||||||
|  |                  attributes: EnemyAttributes): | ||||||
|  |         """Initialise the enemy | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use | ||||||
|  |         :type image_name: str | ||||||
|  |         :param attributes: The attributes of this | ||||||
|  |         :type attributes: EnemyAttributes | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, image_name, attributes) | ||||||
|  |         self.attributes = attributes | ||||||
|  | 
 | ||||||
|  |     def tick(self, player): | ||||||
|  |         """Check for collisions and shoot | ||||||
|  | 
 | ||||||
|  |         :param player: The player which to check collisions with | ||||||
|  |         """ | ||||||
|  |         super().tick() | ||||||
|  |         if self.attributes.cooldown != -1: | ||||||
|  |             self.shoot() | ||||||
|  | 
 | ||||||
|  |         lazer_collisions = self.collide_all(player.lazers) | ||||||
|  |         if lazer_collisions != -1: | ||||||
|  |             self.damage() | ||||||
|  |             player.lazers[lazer_collisions].destroy() | ||||||
|  | 
 | ||||||
|  |         player_collisions = player.collide_all(self.lazers) | ||||||
|  |         if player_collisions != -1: | ||||||
|  |             player.damage() | ||||||
|  |             self.lazers[player_collisions].destroy() | ||||||
|  | 
 | ||||||
|  |         if self.collides(player): | ||||||
|  |             player.damage() | ||||||
|  |             self.damage() | ||||||
|  | 
 | ||||||
|  |     def damage(self, amount=1): | ||||||
|  |         """Reduce the object's health | ||||||
|  | 
 | ||||||
|  |         :param amount: | ||||||
|  |         """ | ||||||
|  |         super().damage(amount) | ||||||
|  |         if self.destroyed: | ||||||
|  |             self.game.score += self.attributes.reward | ||||||
							
								
								
									
										342
									
								
								font.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										342
									
								
								font.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,342 @@ | ||||||
|  | class Font: | ||||||
|  |     """Convert a pixel font into photoimages""" | ||||||
|  | 
 | ||||||
|  |     FONT_SIZE = 5 | ||||||
|  | 
 | ||||||
|  |     # effective width of each character, including letter spacing | ||||||
|  |     FONT_WIDTH = 6 | ||||||
|  | 
 | ||||||
|  |     CHARS = { | ||||||
|  |         "\0": [ | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |         ], | ||||||
|  |         "0": [ | ||||||
|  |             " xxx ", | ||||||
|  |             "x  xx", | ||||||
|  |             "x x x", | ||||||
|  |             "xx  x", | ||||||
|  |             " xxx ", | ||||||
|  |         ], | ||||||
|  |         "1": [ | ||||||
|  |             "  x  ", | ||||||
|  |             " xx  ", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |             "xxxxx", | ||||||
|  |         ], | ||||||
|  |         "2": [ | ||||||
|  |             "xxxx ", | ||||||
|  |             "    x", | ||||||
|  |             " xxx ", | ||||||
|  |             "x    ", | ||||||
|  |             "xxxxx", | ||||||
|  |         ], | ||||||
|  |         "3": [ | ||||||
|  |             "xxxx ", | ||||||
|  |             "    x", | ||||||
|  |             "xxxx ", | ||||||
|  |             "    x", | ||||||
|  |             "xxxx ", | ||||||
|  |         ], | ||||||
|  |         "4": [ | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxxx", | ||||||
|  |             "    x", | ||||||
|  |             "    x", | ||||||
|  |         ], | ||||||
|  |         "5": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "x    ", | ||||||
|  |             " xxx ", | ||||||
|  |             "    x", | ||||||
|  |             "xxxx ", | ||||||
|  |         ], | ||||||
|  |         "6": [ | ||||||
|  |             " xxx ", | ||||||
|  |             "x    ", | ||||||
|  |             "xxxx ", | ||||||
|  |             "x   x", | ||||||
|  |             " xxx ", | ||||||
|  |         ], | ||||||
|  |         "7": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "    x", | ||||||
|  |             "   x ", | ||||||
|  |             "  x  ", | ||||||
|  |             " x   ", | ||||||
|  |         ], | ||||||
|  |         "8": [ | ||||||
|  |             " xxx ", | ||||||
|  |             "x   x", | ||||||
|  |             " xxx ", | ||||||
|  |             "x   x", | ||||||
|  |             " xxx ", | ||||||
|  |         ], | ||||||
|  |         "9": [ | ||||||
|  |             " xxx ", | ||||||
|  |             "x   x", | ||||||
|  |             " xxxx", | ||||||
|  |             "    x", | ||||||
|  |             "    x", | ||||||
|  |         ], | ||||||
|  | 
 | ||||||
|  |         "a": [ | ||||||
|  |             " xxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxxx", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |         ], | ||||||
|  |         "b": [ | ||||||
|  |             "xxxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxx ", | ||||||
|  |         ], | ||||||
|  |         "c": [ | ||||||
|  |             " xxxx", | ||||||
|  |             "x    ", | ||||||
|  |             "x    ", | ||||||
|  |             "x    ", | ||||||
|  |             " xxxx", | ||||||
|  |         ], | ||||||
|  |         "d": [ | ||||||
|  |             "xxxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxx ", | ||||||
|  |         ], | ||||||
|  |         "e": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "x    ", | ||||||
|  |             "xxxxx", | ||||||
|  |             "x    ", | ||||||
|  |             "xxxxx", | ||||||
|  |         ], | ||||||
|  |         "f": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "x    ", | ||||||
|  |             "xxxxx", | ||||||
|  |             "x    ", | ||||||
|  |             "x    ", | ||||||
|  |         ], | ||||||
|  |         "g": [ | ||||||
|  |             " xxxx", | ||||||
|  |             "x    ", | ||||||
|  |             "x  xx", | ||||||
|  |             "x   x", | ||||||
|  |             " xxxx", | ||||||
|  |         ], | ||||||
|  |         "h": [ | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxxx", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |         ], | ||||||
|  |         "i": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |             "xxxxx", | ||||||
|  |         ], | ||||||
|  |         "j": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "    x", | ||||||
|  |             "    x", | ||||||
|  |             "    x", | ||||||
|  |             "xxxx ", | ||||||
|  |         ], | ||||||
|  |         "k": [ | ||||||
|  |             "x   x", | ||||||
|  |             "x  x ", | ||||||
|  |             "xxx  ", | ||||||
|  |             "x  x ", | ||||||
|  |             "x   x", | ||||||
|  |         ], | ||||||
|  |         "l": [ | ||||||
|  |             "x    ", | ||||||
|  |             "x    ", | ||||||
|  |             "x    ", | ||||||
|  |             "x    ", | ||||||
|  |             "xxxxx", | ||||||
|  |         ], | ||||||
|  |         "m": [ | ||||||
|  |             "x   x", | ||||||
|  |             "xx xx", | ||||||
|  |             "x x x", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |         ], | ||||||
|  |         "n": [ | ||||||
|  |             "x   x", | ||||||
|  |             "xx  x", | ||||||
|  |             "x x x", | ||||||
|  |             "x  xx", | ||||||
|  |             "x   x", | ||||||
|  |         ], | ||||||
|  |         "o": [ | ||||||
|  |             " xxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             " xxx ", | ||||||
|  |         ], | ||||||
|  |         "p": [ | ||||||
|  |             "xxxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxx ", | ||||||
|  |             "x    ", | ||||||
|  |             "x    ", | ||||||
|  |         ], | ||||||
|  |         "q": [ | ||||||
|  |             " xxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "x  x ", | ||||||
|  |             " xx x", | ||||||
|  |         ], | ||||||
|  |         "r": [ | ||||||
|  |             "xxxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "xxxx ", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |         ], | ||||||
|  |         "s": [ | ||||||
|  |             " xxxx", | ||||||
|  |             "x    ", | ||||||
|  |             " xxx ", | ||||||
|  |             "    x", | ||||||
|  |             "xxxx ", | ||||||
|  |         ], | ||||||
|  |         "t": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |         ], | ||||||
|  |         "u": [ | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             " xxx ", | ||||||
|  |         ], | ||||||
|  |         "v": [ | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             " x x ", | ||||||
|  |             "  x  ", | ||||||
|  |         ], | ||||||
|  |         "w": [ | ||||||
|  |             "x   x", | ||||||
|  |             "x   x", | ||||||
|  |             "x x x", | ||||||
|  |             "x x x", | ||||||
|  |             " x x ", | ||||||
|  |         ], | ||||||
|  |         "x": [ | ||||||
|  |             "x   x", | ||||||
|  |             " x x ", | ||||||
|  |             "  x  ", | ||||||
|  |             " x x ", | ||||||
|  |             "x   x", | ||||||
|  |         ], | ||||||
|  |         "y": [ | ||||||
|  |             "x   x", | ||||||
|  |             " x x ", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |             "  x  ", | ||||||
|  |         ], | ||||||
|  |         "z": [ | ||||||
|  |             "xxxxx", | ||||||
|  |             "   x ", | ||||||
|  |             "  x  ", | ||||||
|  |             " x   ", | ||||||
|  |             "xxxxx", | ||||||
|  |         ], | ||||||
|  |         " ": [ | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |             "     ", | ||||||
|  |         ], | ||||||
|  |         ">": [ | ||||||
|  |             " x   ", | ||||||
|  |             "  x  ", | ||||||
|  |             "   x ", | ||||||
|  |             "  x  ", | ||||||
|  |             " x   ", | ||||||
|  |         ], | ||||||
|  |         "<": [ | ||||||
|  |             "   x ", | ||||||
|  |             "  x  ", | ||||||
|  |             " x   ", | ||||||
|  |             "  x  ", | ||||||
|  |             "   x ", | ||||||
|  |         ], | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def _create_font_texture(text, color="#fff", letter_space=1): | ||||||
|  |         """Convert a font array into a game texture | ||||||
|  | 
 | ||||||
|  |         :param text: the characters used within the font | ||||||
|  |         :param color: The colour to use | ||||||
|  |         :param letter_space: The spacing between each letter to use | ||||||
|  |         """ | ||||||
|  |         string = Font._create_characters(text, letter_space) | ||||||
|  |         return [ | ||||||
|  |             [ | ||||||
|  |                 None if character == " " else color | ||||||
|  |                 for character in row | ||||||
|  |             ] for row in string | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def _create_characters(text, letter_space): | ||||||
|  |         """Concatenate font symbols | ||||||
|  | 
 | ||||||
|  |         :param text: The text of the font | ||||||
|  |         :param letter_space: The spacing between each letter | ||||||
|  |         """ | ||||||
|  |         # create a list of all characters in the string | ||||||
|  |         characters = [ | ||||||
|  |             Font.CHARS[c] if c in Font.CHARS else Font.CHARS["\0"] | ||||||
|  |             for c in text.lower() | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |         # join each row of each character into one "character" | ||||||
|  |         return [ | ||||||
|  |             (" "*letter_space).join([c[row] for c in characters]) | ||||||
|  |             for row in range(Font.FONT_SIZE) | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def load_text(texture_factory, text, color="#fff", letter_space=1): | ||||||
|  |         """Create and load text into a photo image | ||||||
|  | 
 | ||||||
|  |         :param texture_factory: The texture factory used for processing | ||||||
|  |         :param text: The text to convert | ||||||
|  |         :param color: Color of the text | ||||||
|  |         :param letter_space: Spacing between letters | ||||||
|  |         """ | ||||||
|  |         return texture_factory.load_texture(f"text:{text}", | ||||||
|  |                                             Font._create_font_texture( | ||||||
|  |                                                 text, | ||||||
|  |                                                 color=color, | ||||||
|  |                                                 letter_space=letter_space) | ||||||
|  |                                             ) | ||||||
							
								
								
									
										296
									
								
								formation.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								formation.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,296 @@ | ||||||
|  | from dataclasses import dataclass | ||||||
|  | import math | ||||||
|  | from typing import List | ||||||
|  | 
 | ||||||
|  | from enemy import Enemy, EnemyAttributes | ||||||
|  | from game import Game | ||||||
|  | from sprite import Sprite | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class FormationAttributes(EnemyAttributes): | ||||||
|  |     """FormationAttributes.""" | ||||||
|  | 
 | ||||||
|  |     count: int = 1 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FormationEnemy(Enemy): | ||||||
|  |     """An enemy that belongs to a formation""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name, offset, | ||||||
|  |                  attributes: EnemyAttributes): | ||||||
|  |         """Initialise the enemy | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for the enemy | ||||||
|  |         :param offset: The offset from the other enemies | ||||||
|  |         :param attributes: The attributes given to this enemy | ||||||
|  |         :type attributes: EnemyAttributes | ||||||
|  |         """ | ||||||
|  |         self.offset_x, self.offset_y, self.offset_a = offset | ||||||
|  |         super().__init__(game, image_name, attributes) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EnemyFormation: | ||||||
|  |     """Cluster of enemies that move in a particular way""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name: str, | ||||||
|  |                  enemy_attributes: FormationAttributes): | ||||||
|  |         """Initialise the formation | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for the enemy | ||||||
|  |         :type image_name: str | ||||||
|  |         :param enemy_attributes: The attributes to use for spawned enemies | ||||||
|  |         :type enemy_attributes: FormationAttributes | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.sprites: List[FormationEnemy] = [] | ||||||
|  |         self.image_name = image_name | ||||||
|  | 
 | ||||||
|  |         self.alpha = 0 | ||||||
|  |         self.attributes = enemy_attributes | ||||||
|  | 
 | ||||||
|  |         self.x, self.y = 0, 0 | ||||||
|  |         self.destroyed = False | ||||||
|  | 
 | ||||||
|  |         self.create_enemies() | ||||||
|  |         self.hidden = True | ||||||
|  | 
 | ||||||
|  |     def create_enemies(self): | ||||||
|  |         """Spawn enemies""" | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def position_enemy(self, enemy: FormationEnemy): | ||||||
|  |         """Position a single enemy | ||||||
|  | 
 | ||||||
|  |         :param enemy: The enemy to position | ||||||
|  |         :type enemy: FormationEnemy | ||||||
|  |         """ | ||||||
|  |         enemy.set_pos( | ||||||
|  |             ( | ||||||
|  |                 int(self.x + enemy.offset_x), | ||||||
|  |                 int(self.y + enemy.offset_y) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def spawn_enemy(self, offset): | ||||||
|  |         """Spawn a single enemy | ||||||
|  | 
 | ||||||
|  |         :param offset: The offset which to apply to the enemy | ||||||
|  |         """ | ||||||
|  |         enemy = FormationEnemy(self.game, self.image_name, | ||||||
|  |                                offset, self.attributes) | ||||||
|  |         self.sprites.append(enemy) | ||||||
|  |         return enemy | ||||||
|  | 
 | ||||||
|  |     def tick(self, player): | ||||||
|  |         """Update the positions of all enemies | ||||||
|  | 
 | ||||||
|  |         :param player: The player to check if the enemies collide with | ||||||
|  |         """ | ||||||
|  |         self.alpha += 1 | ||||||
|  |         for enemy in self.sprites: | ||||||
|  |             enemy.tick(player) | ||||||
|  |             self.position_enemy(enemy) | ||||||
|  |         self.sprites = Sprite.remove_destroyed(self.sprites) | ||||||
|  |         if len(self.sprites) == 0: | ||||||
|  |             self.destroy() | ||||||
|  | 
 | ||||||
|  |     def destroy(self): | ||||||
|  |         """Delete all enemies in this formation""" | ||||||
|  |         for enemy in self.sprites: | ||||||
|  |             enemy.destroy() | ||||||
|  |         self.sprites = [] | ||||||
|  |         self.destroyed = True | ||||||
|  | 
 | ||||||
|  |     def set_pos(self, pos): | ||||||
|  |         """Set the position of this formation | ||||||
|  | 
 | ||||||
|  |         :param pos: position to move to | ||||||
|  |         """ | ||||||
|  |         self.x, self.y = pos | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Make this formation visible""" | ||||||
|  |         if self.hidden: | ||||||
|  |             for enemy in self.sprites: | ||||||
|  |                 enemy.show() | ||||||
|  |             self.hidden = False | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Make this formation hidden""" | ||||||
|  |         if not self.hidden: | ||||||
|  |             for enemy in self.sprites: | ||||||
|  |                 enemy.hide() | ||||||
|  |             self.hidden = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class CircleFormationAttributes(FormationAttributes): | ||||||
|  |     """Attributes for a circle formation""" | ||||||
|  | 
 | ||||||
|  |     radius: int = 40 | ||||||
|  |     period: int = 300 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CircleFormation(EnemyFormation): | ||||||
|  |     """A circular formation of enemies, rotating in a ring""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name, | ||||||
|  |                  attributes: CircleFormationAttributes): | ||||||
|  |         """Initialise the formation | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for the enemy | ||||||
|  |         :param attributes: The attributes to use for spawned enemies | ||||||
|  |         :type attributes: CircleFormationAttributes | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, image_name, attributes) | ||||||
|  |         self.attributes: CircleFormationAttributes | ||||||
|  | 
 | ||||||
|  |     def create_enemies(self): | ||||||
|  |         """Spawn all the enemies""" | ||||||
|  |         for i in range(self.attributes.count): | ||||||
|  |             self.spawn_enemy((0, 0, i)) | ||||||
|  | 
 | ||||||
|  |     def position_enemy(self, enemy: FormationEnemy): | ||||||
|  |         """Position a single enemy | ||||||
|  | 
 | ||||||
|  |         :param enemy: | ||||||
|  |         :type enemy: FormationEnemy | ||||||
|  |         """ | ||||||
|  |         a = (enemy.offset_a / self.attributes.count) * \ | ||||||
|  |             self.attributes.period + self.game.alpha | ||||||
|  |         enemy.set_pos( | ||||||
|  |             ( | ||||||
|  |                 int( | ||||||
|  |                     self.x+math.sin((-a/self.attributes.period) | ||||||
|  |                                     * 2*math.pi) * self.attributes.radius | ||||||
|  |                 ), | ||||||
|  |                 int( | ||||||
|  |                     self.y+math.cos((-a/self.attributes.period) | ||||||
|  |                                     * 2*math.pi) * self.attributes.radius | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class LemniscateFormation(EnemyFormation): | ||||||
|  |     """An 'infinity' shape enemy formation""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name, | ||||||
|  |                  attributes: CircleFormationAttributes): | ||||||
|  |         """Initialise the formation | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for the enemy | ||||||
|  |         :param attributes: The attributes to use for spawned enemies | ||||||
|  |         :type attributes: CircleFormationAttributes | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, image_name, attributes) | ||||||
|  |         self.attributes: CircleFormationAttributes | ||||||
|  | 
 | ||||||
|  |     def create_enemies(self): | ||||||
|  |         """Spawn all enemies""" | ||||||
|  |         for i in range(self.attributes.count): | ||||||
|  |             self.spawn_enemy((0, 0, (i / self.attributes.count) | ||||||
|  |                              * self.attributes.period * 0.25)) | ||||||
|  | 
 | ||||||
|  |     def position_enemy(self, enemy: FormationEnemy): | ||||||
|  |         """Position an enemy | ||||||
|  | 
 | ||||||
|  |         :param enemy: | ||||||
|  |         :type enemy: FormationEnemy | ||||||
|  |         """ | ||||||
|  |         a = enemy.offset_a + self.game.alpha | ||||||
|  | 
 | ||||||
|  |         t = (-a/self.attributes.period)*2*math.pi | ||||||
|  |         x = self.x+(self.attributes.radius * math.cos(t)) / \ | ||||||
|  |             (1 + math.sin(t)**2) | ||||||
|  |         y = self.y+(self.attributes.radius * math.sin(t) * math.cos(t)) / \ | ||||||
|  |             (1 + math.sin(t)**2) | ||||||
|  | 
 | ||||||
|  |         enemy.set_pos( | ||||||
|  |             ( | ||||||
|  |                 int(x), | ||||||
|  |                 int(y) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class TriangleFormationAttributes(FormationAttributes): | ||||||
|  |     """Attributes for a triangular formation""" | ||||||
|  | 
 | ||||||
|  |     spacing: int = 16 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TriangleFormation(EnemyFormation): | ||||||
|  |     """A v-shaped formation of enemies""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name: str, | ||||||
|  |                  attributes: TriangleFormationAttributes): | ||||||
|  |         """Initialise the formation | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for the enemy | ||||||
|  |         :type image_name: str | ||||||
|  |         :param attributes: The attributes to use for spawned enemies | ||||||
|  |         :type attributes: TriangleFormationAttributes | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, image_name, attributes) | ||||||
|  |         self.attributes: TriangleFormationAttributes | ||||||
|  | 
 | ||||||
|  |     def create_enemies(self): | ||||||
|  |         """Spawn all enemies in this formation""" | ||||||
|  |         for i in range(self.attributes.count): | ||||||
|  |             y = -((i+1) // 2)*self.attributes.spacing//2 | ||||||
|  | 
 | ||||||
|  |             # first part is multiply by 1 or -1 to determine the side | ||||||
|  |             # then just have an offset for how far | ||||||
|  |             x = 2*((i % 2)-0.5) * ((i+1)//2)*self.attributes.spacing | ||||||
|  | 
 | ||||||
|  |             self.spawn_enemy((x, y, 1)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class RectangleFormationAttributes(TriangleFormationAttributes): | ||||||
|  |     """Attributes for a rectangle formation""" | ||||||
|  | 
 | ||||||
|  |     width: int = 5 | ||||||
|  |     height: int = 2 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RectangleFormation(EnemyFormation): | ||||||
|  |     """A grid-like formation of enemies""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name, | ||||||
|  |                  attributes: RectangleFormationAttributes): | ||||||
|  |         """Initialise the formation | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for the enemy | ||||||
|  |         :param attributes: The attributes to use for spawned enemies | ||||||
|  |         :type attributes: RectangleFormationAttributes | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, image_name, attributes) | ||||||
|  |         self.attributes: RectangleFormationAttributes | ||||||
|  | 
 | ||||||
|  |     def create_enemies(self): | ||||||
|  |         """Spawn all enemies""" | ||||||
|  |         full_width = self.attributes.width * self.attributes.spacing | ||||||
|  |         full_height = self.attributes.height * self.attributes.spacing | ||||||
|  | 
 | ||||||
|  |         for y in range(self.attributes.height): | ||||||
|  |             offset_y = ((y+0.5)*self.attributes.spacing)-(full_height/2) | ||||||
|  | 
 | ||||||
|  |             for x in range(self.attributes.width): | ||||||
|  |                 offset_x = ((x+0.5)*self.attributes.spacing)-(full_width/2) | ||||||
|  |                 self.spawn_enemy((offset_x, offset_y, 1)) | ||||||
							
								
								
									
										341
									
								
								formation_spawner.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								formation_spawner.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,341 @@ | ||||||
|  | import math | ||||||
|  | from random import choice, randint, random | ||||||
|  | 
 | ||||||
|  | from boss import CircleBossFormation, SnakeBossFormation | ||||||
|  | from enemy import EnemyAttributes | ||||||
|  | from formation import ( | ||||||
|  |     CircleFormation, | ||||||
|  |     CircleFormationAttributes, | ||||||
|  |     EnemyFormation, | ||||||
|  |     FormationAttributes, | ||||||
|  |     LemniscateFormation, | ||||||
|  |     RectangleFormation, | ||||||
|  |     RectangleFormationAttributes, | ||||||
|  |     TriangleFormation, | ||||||
|  |     TriangleFormationAttributes, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def wobble_pattern(formation): | ||||||
|  |     """A sinusoidal movement pattern | ||||||
|  | 
 | ||||||
|  |     :param formation: Formation to move | ||||||
|  |     """ | ||||||
|  |     x = (1+math.sin(formation.alpha/80)) * formation.game.w/2 | ||||||
|  |     y = formation.y + (1 if formation.alpha % 4 == 0 else 0) | ||||||
|  |     formation.set_pos((x, y)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def speed_pattern(formation): | ||||||
|  |     """Quickly move the formation downwards | ||||||
|  | 
 | ||||||
|  |     :param formation: Formation to move | ||||||
|  |     """ | ||||||
|  |     x = formation.x | ||||||
|  |     y = formation.y + 2 | ||||||
|  |     formation.set_pos((x, y)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def slow_pattern(formation): | ||||||
|  |     """Slowly move the formation downwards | ||||||
|  | 
 | ||||||
|  |     :param formation: Formation to move | ||||||
|  |     """ | ||||||
|  |     x = formation.x | ||||||
|  |     y = formation.y + (1 if formation.alpha % 8 == 0 else 0) | ||||||
|  |     formation.set_pos((x, y)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def slide_in_pattern(formation): | ||||||
|  |     """Slowly move into the center of the screen and then remain there | ||||||
|  | 
 | ||||||
|  |     :param formation: Formation to move | ||||||
|  |     """ | ||||||
|  |     cy = formation.game.h//3 | ||||||
|  | 
 | ||||||
|  |     if formation.alpha < 400: | ||||||
|  |         x = formation.x | ||||||
|  |         y = (formation.alpha/400) * (cy*1.5) - (cy*0.5) | ||||||
|  |     else: | ||||||
|  |         x = formation.x | ||||||
|  |         y = formation.y | ||||||
|  | 
 | ||||||
|  |     formation.set_pos((int(x), int(y))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def no_pattern(formation): | ||||||
|  |     """No movement, stay in the center | ||||||
|  | 
 | ||||||
|  |     :param formation: Formation to move | ||||||
|  |     """ | ||||||
|  |     formation.set_pos(( | ||||||
|  |         formation.game.w // 2, | ||||||
|  |         formation.game.h // 3 | ||||||
|  |     )) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def figure_of_eight_pattern(formation): | ||||||
|  |     """Move the formation in a figure of eight | ||||||
|  | 
 | ||||||
|  |     :param formation: Formation to move | ||||||
|  |     """ | ||||||
|  |     period = 600 | ||||||
|  |     edge = 8 | ||||||
|  |     radius = formation.game.h//3 - edge | ||||||
|  |     cx, cy = formation.game.w//2, formation.game.h//3 | ||||||
|  | 
 | ||||||
|  |     if formation.alpha < 200: | ||||||
|  |         x = formation.x | ||||||
|  |         y = (formation.alpha/200) * (cy*1.5) - (cy*0.5) | ||||||
|  |     else: | ||||||
|  |         a = formation.alpha - 200 | ||||||
|  |         t = (a/period)*2*math.pi - math.pi/2 | ||||||
|  | 
 | ||||||
|  |         y = cy + (radius * math.cos(t)) / (1 + math.sin(t)**2) | ||||||
|  |         x = cx + (radius * math.sin(t) * math.cos(t)) / (1 + math.sin(t)**2) | ||||||
|  | 
 | ||||||
|  |     formation.set_pos((int(x), int(y))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FormationSpawner(): | ||||||
|  |     """Object to manage spawning of enemies and phases""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game): | ||||||
|  |         """Initialise the formation spawner | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.formations = [] | ||||||
|  |         self.difficulty_multiplier = 0.5 | ||||||
|  | 
 | ||||||
|  |         self.next_formation = 0 | ||||||
|  | 
 | ||||||
|  |         self.phase = -1 | ||||||
|  |         self.phases = [ | ||||||
|  |             Phase("Phase:1", [ | ||||||
|  |                   self.spawn_fleet, | ||||||
|  |                   self.spawn_loop, | ||||||
|  |                   self.spawn_orbital], 10), | ||||||
|  |             Phase("Boss:1", [self.spawn_circle_boss], 1), | ||||||
|  |             Phase("Phase:2", [ | ||||||
|  |                   self.spawn_fleet, | ||||||
|  |                   self.spawn_loop, | ||||||
|  |                   self.spawn_orbital], 10, max_wave=3), | ||||||
|  |             Phase("Boss:2", [self.spawn_snake_boss], 1), | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |         self.to_spawn = 0 | ||||||
|  |         self.current_reward = 1 | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update all formations""" | ||||||
|  |         for formation, update in self.formations: | ||||||
|  |             formation.tick(self.game.player) | ||||||
|  |             update(formation) | ||||||
|  |             if formation.y > self.game.h: | ||||||
|  |                 formation.destroy() | ||||||
|  |         self.formations = list( | ||||||
|  |             filter(lambda s: not s[0].destroyed, self.formations)) | ||||||
|  | 
 | ||||||
|  |         self.spawn_next() | ||||||
|  | 
 | ||||||
|  |     def spawn_random(self): | ||||||
|  |         """Spawn a random formation""" | ||||||
|  |         options = [ | ||||||
|  |             self.spawn_fleet, | ||||||
|  |             self.spawn_loop, | ||||||
|  |             self.spawn_orbital, | ||||||
|  |             self.spawn_rectangle | ||||||
|  |         ] | ||||||
|  |         choice(options)() | ||||||
|  | 
 | ||||||
|  |     def spawn_formation(self, formation: EnemyFormation, update): | ||||||
|  |         """Add a formation to the list of formations | ||||||
|  | 
 | ||||||
|  |         :param formation: Formation to add | ||||||
|  |         :type formation: EnemyFormation | ||||||
|  |         :param update: movement function to use for this formation | ||||||
|  |         """ | ||||||
|  |         update(formation) | ||||||
|  |         formation.show() | ||||||
|  |         self.formations.append((formation, update)) | ||||||
|  | 
 | ||||||
|  |     def spawn_circle_boss(self): | ||||||
|  |         """Spawn the circle boss""" | ||||||
|  |         attributes = EnemyAttributes( | ||||||
|  |             hp=int(15*self.difficulty_multiplier), | ||||||
|  |             reward=self.current_reward, | ||||||
|  |             cooldown=50 | ||||||
|  |         ) | ||||||
|  |         formation = CircleBossFormation(self.game, attributes) | ||||||
|  |         formation.set_pos((self.game.w//2, 0)) | ||||||
|  |         update = figure_of_eight_pattern | ||||||
|  |         self.spawn_formation(formation, update) | ||||||
|  | 
 | ||||||
|  |     def spawn_snake_boss(self): | ||||||
|  |         """Spawn the snake boss""" | ||||||
|  |         attributes = FormationAttributes( | ||||||
|  |             hp=int(10*self.difficulty_multiplier), | ||||||
|  |             reward=self.current_reward, | ||||||
|  |             cooldown=160 | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         formation = SnakeBossFormation(self.game, attributes) | ||||||
|  |         formation.set_pos((self.game.w//2, 0)) | ||||||
|  |         update = slide_in_pattern | ||||||
|  |         self.spawn_formation(formation, update) | ||||||
|  | 
 | ||||||
|  |     def spawn_fleet(self): | ||||||
|  |         """Spawn the fleet formation""" | ||||||
|  |         sprite = randint(6, 7) | ||||||
|  | 
 | ||||||
|  |         position = (random()*self.game.w, -32) | ||||||
|  |         attributes = TriangleFormationAttributes( | ||||||
|  |             hp=int(self.difficulty_multiplier), | ||||||
|  |             cooldown=-1, | ||||||
|  |             reward=self.current_reward, | ||||||
|  |             count=randint(1, 3)*2 + 1, | ||||||
|  |             spacing=8 | ||||||
|  |         ) | ||||||
|  |         formation = TriangleFormation( | ||||||
|  |             self.game, f"smallenemy{sprite}", attributes) | ||||||
|  |         formation.set_pos(position) | ||||||
|  | 
 | ||||||
|  |         update = speed_pattern | ||||||
|  |         self.spawn_formation(formation, update) | ||||||
|  | 
 | ||||||
|  |     def spawn_orbital(self): | ||||||
|  |         """Spawn the orbital formation""" | ||||||
|  |         position = (random()*self.game.w, -32) | ||||||
|  |         sprite = choice((1, 3)) | ||||||
|  | 
 | ||||||
|  |         attributes = CircleFormationAttributes( | ||||||
|  |             hp=int(self.difficulty_multiplier * 2), | ||||||
|  |             count=randint(3, 4)*2, | ||||||
|  |             radius=randint(10, 20), | ||||||
|  |             period=randint(100//int(self.difficulty_multiplier), 400), | ||||||
|  |             cooldown=80, | ||||||
|  |             reward=self.current_reward | ||||||
|  | 
 | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         formation = CircleFormation( | ||||||
|  |             self.game, f"smallenemy{sprite}", attributes) | ||||||
|  |         formation.set_pos(position) | ||||||
|  | 
 | ||||||
|  |         update = wobble_pattern | ||||||
|  |         formation.alpha = randint(1, 1000) | ||||||
|  |         self.spawn_formation(formation, update) | ||||||
|  | 
 | ||||||
|  |     def spawn_rectangle(self): | ||||||
|  |         """Spawn the rectangle formation""" | ||||||
|  |         sprite = choice((0, 2)) | ||||||
|  |         position = (random() * self.game.w, -32) | ||||||
|  | 
 | ||||||
|  |         attributes = RectangleFormationAttributes( | ||||||
|  |             hp=int(self.difficulty_multiplier * 2), | ||||||
|  |             width=randint(4, 6), | ||||||
|  |             height=randint(2, 3), | ||||||
|  |             cooldown=80, | ||||||
|  |             reward=self.current_reward, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         formation = RectangleFormation( | ||||||
|  |             self.game, f"smallenemy{sprite}", attributes | ||||||
|  |         ) | ||||||
|  |         formation.set_pos(position) | ||||||
|  | 
 | ||||||
|  |         update = wobble_pattern | ||||||
|  |         formation.alpha = randint(1, 1000) | ||||||
|  |         self.spawn_formation(formation, update) | ||||||
|  | 
 | ||||||
|  |     def spawn_loop(self): | ||||||
|  |         """Spawn the loop formation""" | ||||||
|  |         sprite = choice((4, 5)) | ||||||
|  |         position = (random()*self.game.w, -32) | ||||||
|  |         attributes = CircleFormationAttributes( | ||||||
|  |             count=randint(4, 8), | ||||||
|  |             radius=randint(self.game.w//2, self.game.w), | ||||||
|  |             period=randint(200, 300), | ||||||
|  |             hp=int(self.difficulty_multiplier), | ||||||
|  |             reward=self.current_reward, | ||||||
|  |             cooldown=160, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         formation = LemniscateFormation( | ||||||
|  |             self.game, f"smallenemy{sprite}", attributes) | ||||||
|  |         formation.set_pos(position) | ||||||
|  | 
 | ||||||
|  |         update = slow_pattern | ||||||
|  |         self.spawn_formation(formation, update) | ||||||
|  | 
 | ||||||
|  |     def spawn_next(self): | ||||||
|  |         """Spawn the next formation to be spawned""" | ||||||
|  |         if self.to_spawn > 0: | ||||||
|  |             if len(self.formations) < self.current_phase().max_wave: | ||||||
|  |                 if self.game.alpha > self.next_formation \ | ||||||
|  |                         and self.next_formation != -1: | ||||||
|  |                     self.next_formation = self.game.alpha \ | ||||||
|  |                             + 100 / self.difficulty_multiplier | ||||||
|  | 
 | ||||||
|  |                     self.current_phase().get_spawn_function()() | ||||||
|  |                     self.to_spawn -= 1 | ||||||
|  |         else: | ||||||
|  |             if len(self.formations) == 0: | ||||||
|  |                 self.next_phase() | ||||||
|  | 
 | ||||||
|  |     def next_phase(self): | ||||||
|  |         """Increment the phase by 1 and start the next phase""" | ||||||
|  |         self.phase += 1 | ||||||
|  |         self.game.save_game() | ||||||
|  |         self.start_phase() | ||||||
|  | 
 | ||||||
|  |     def start_phase(self): | ||||||
|  |         """Start the next phase""" | ||||||
|  |         self.to_spawn = self.current_phase().duration | ||||||
|  | 
 | ||||||
|  |         self.difficulty_multiplier = (self.phase+2) * 0.5 | ||||||
|  |         self.current_reward = int(2**self.difficulty_multiplier) | ||||||
|  | 
 | ||||||
|  |         self.next_formation = self.game.alpha + 100 | ||||||
|  |         if self.current_phase().name: | ||||||
|  |             self.game.effect_player.splash_text(self.current_phase().name) | ||||||
|  | 
 | ||||||
|  |     def current_phase(self): | ||||||
|  |         """Return the current phase""" | ||||||
|  |         if self.phase < len(self.phases): | ||||||
|  |             return self.phases[self.phase] | ||||||
|  | 
 | ||||||
|  |         return Phase(f"Phase:{self.phase-1}", [ | ||||||
|  |             self.spawn_random | ||||||
|  |         ], 10 * self.difficulty_multiplier, | ||||||
|  |             max_wave=int(self.difficulty_multiplier) | ||||||
|  |                      ) | ||||||
|  | 
 | ||||||
|  |     def clear_all(self): | ||||||
|  |         """Remove all formation objects""" | ||||||
|  |         for f, _ in self.formations: | ||||||
|  |             f.destroy() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Phase: | ||||||
|  |     """Rules for which formation will be spawned""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, name, spawn_functions, duration, max_wave=2): | ||||||
|  |         """__init__. | ||||||
|  | 
 | ||||||
|  |         :param name: The name of the phase | ||||||
|  |         :param spawn_functions:  A list of functions to use to spawn enemies | ||||||
|  |         :param duration: The number of formations to spawn | ||||||
|  |                          before the phase is over | ||||||
|  |         :param max_wave: The maximum number of formations to spawn at a time | ||||||
|  |         """ | ||||||
|  |         self.spawn_functions = spawn_functions | ||||||
|  |         self.duration = duration | ||||||
|  |         self.name = name | ||||||
|  |         self.max_wave = max_wave | ||||||
|  | 
 | ||||||
|  |     def get_spawn_function(self): | ||||||
|  |         """Return a random spawn function""" | ||||||
|  |         return choice(self.spawn_functions) | ||||||
							
								
								
									
										41
									
								
								frame_counter.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frame_counter.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | from sys import stderr | ||||||
|  | from time import time | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FrameCounter: | ||||||
|  |     """Creates a main loop and ensures that the framerate is static""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, canvas, target_fps): | ||||||
|  |         """Initialise the frame counter | ||||||
|  | 
 | ||||||
|  |         :param canvas: The canvas which to call after on | ||||||
|  |         :param target_fps: The fps to aim to achieve | ||||||
|  |         """ | ||||||
|  |         self.canvas = canvas | ||||||
|  |         self.fps = target_fps | ||||||
|  |         self.frame_time = 1 / target_fps | ||||||
|  |         self.last_frame = time() | ||||||
|  | 
 | ||||||
|  |         self.current_fps = 1 | ||||||
|  | 
 | ||||||
|  |     def next_frame(self, callback): | ||||||
|  |         """Calculate when the next frame should be called | ||||||
|  | 
 | ||||||
|  |         :param callback: function to call for the next frame | ||||||
|  |         """ | ||||||
|  |         t = time() | ||||||
|  |         ft = t - self.last_frame | ||||||
|  | 
 | ||||||
|  |         delay = 0 | ||||||
|  | 
 | ||||||
|  |         if ft > self.frame_time: | ||||||
|  |             if ft - self.frame_time > self.frame_time / 5: | ||||||
|  |                 print( | ||||||
|  |                     f"Help! Running {ft - self.frame_time} seconds behind!", | ||||||
|  |                     file=stderr) | ||||||
|  |         else: | ||||||
|  |             delay = self.frame_time - ft | ||||||
|  | 
 | ||||||
|  |         self.canvas.after(int(delay*1000), callback) | ||||||
|  |         self.current_fps = 1 / (delay+ft) | ||||||
|  |         self.last_frame = t | ||||||
							
								
								
									
										310
									
								
								game.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								game.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,310 @@ | ||||||
|  | from random import randint, random | ||||||
|  | from tkinter import Canvas, PhotoImage, Tk | ||||||
|  | from typing import List | ||||||
|  | 
 | ||||||
|  | from config import Config | ||||||
|  | from font import Font | ||||||
|  | from frame_counter import FrameCounter | ||||||
|  | from inputs import InputController | ||||||
|  | from sprite import Sprite | ||||||
|  | from textures import TextureFactory | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Game: | ||||||
|  |     """A generic game object""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self) -> None: | ||||||
|  |         """Initialise the game | ||||||
|  |         """ | ||||||
|  |         self.win = Tk() | ||||||
|  |         game_width, game_height = ( | ||||||
|  |             Config.WIDTH*Config.SCALE, Config.HEIGHT*Config.SCALE | ||||||
|  |         ) | ||||||
|  |         self.w, self.h = Config.WIDTH, Config.HEIGHT | ||||||
|  | 
 | ||||||
|  |         self.win.geometry(f"{game_width}x{game_height}") | ||||||
|  |         self.canvas = Canvas(self.win, width=game_width, | ||||||
|  |                              height=game_height, bg="#000") | ||||||
|  |         self.canvas.pack() | ||||||
|  | 
 | ||||||
|  |         self.texture_factory = TextureFactory(scale=Config.SCALE) | ||||||
|  |         self.effect_player = EffectPlayer(self) | ||||||
|  |         self.frame_counter = FrameCounter(self.canvas, Config.FPS) | ||||||
|  | 
 | ||||||
|  |         self.inputs = InputController(self) | ||||||
|  |         self.sprites = [] | ||||||
|  | 
 | ||||||
|  |         self.score = 0 | ||||||
|  | 
 | ||||||
|  |         self.alpha = 0 | ||||||
|  | 
 | ||||||
|  |     def start(self): | ||||||
|  |         """Start the game""" | ||||||
|  |         self.loop() | ||||||
|  |         self.win.mainloop() | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update the game's sprites""" | ||||||
|  |         for sprite in self.sprites: | ||||||
|  |             sprite.tick() | ||||||
|  |         self.effect_player.tick() | ||||||
|  | 
 | ||||||
|  |     def loop(self): | ||||||
|  |         """Loop the game at a set framerate""" | ||||||
|  |         self.alpha += 1 | ||||||
|  |         self.tick() | ||||||
|  |         self.frame_counter.next_frame(self.loop) | ||||||
|  | 
 | ||||||
|  |     def clear_all(self): | ||||||
|  |         """Remove all game sprites""" | ||||||
|  |         for sprite in self.sprites: | ||||||
|  |             sprite.destroy() | ||||||
|  |         self.sprites = [] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class GameSprite(Sprite): | ||||||
|  |     """A sprite which belongs to a game""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image: PhotoImage): | ||||||
|  |         """Initialise the sprite | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image: The image to use for the sprite | ||||||
|  |         :type image: PhotoImage | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         super().__init__(game.canvas, image, (0, 0)) | ||||||
|  | 
 | ||||||
|  |     def move(self, x, y): | ||||||
|  |         """Move the sprite by a certain amount | ||||||
|  | 
 | ||||||
|  |         :param x: Amount of pixels to move right | ||||||
|  |         :param y: Amount of pixels to move down | ||||||
|  |         """ | ||||||
|  |         # if the object needs to move less than a pixel | ||||||
|  |         # only move it every few frames to create this effect | ||||||
|  |         if abs(x) >= 1: | ||||||
|  |             self.x += x | ||||||
|  |         elif x != 0: | ||||||
|  |             if self.game.alpha % (1/x) == 0: | ||||||
|  |                 self.x += 1 | ||||||
|  | 
 | ||||||
|  |         if abs(y) >= 1: | ||||||
|  |             self.y += y | ||||||
|  |         elif y != 0: | ||||||
|  |             if self.game.alpha % (1/y) == 0: | ||||||
|  |                 self.y += 1 | ||||||
|  | 
 | ||||||
|  |         self.update_position() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class GameEffect(GameSprite): | ||||||
|  |     """An effect that can be played within game""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image: PhotoImage, | ||||||
|  |                  duration=10, momentum=(0, 0)): | ||||||
|  |         """Initialise the game effect | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image: The image to use for this | ||||||
|  |         :type image: PhotoImage | ||||||
|  |         :param duration: How long this effect should last for | ||||||
|  |         :param momentum: Which direction to move this effect | ||||||
|  |         """ | ||||||
|  |         self.start_time = game.alpha | ||||||
|  |         self.duration = duration | ||||||
|  |         self.velocity_x, self.velocity_y = momentum | ||||||
|  |         super().__init__(game, image) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Move the effect by its momentum and remove it if its over""" | ||||||
|  |         super().tick() | ||||||
|  |         self.move(self.velocity_x, self.velocity_y) | ||||||
|  | 
 | ||||||
|  |         alpha = self.game.alpha - self.start_time | ||||||
|  |         if self.duration != -1 and alpha > self.duration: | ||||||
|  |             self.destroy() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AnimatedEffect(GameEffect): | ||||||
|  |     """An effect which involves animating an image""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, images: List[PhotoImage], | ||||||
|  |                  frame_time=1, momentum=(0, 0)): | ||||||
|  |         """Initialise the effect | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param images: The images to use for this animation | ||||||
|  |         :type images: List[PhotoImage] | ||||||
|  |         :param frame_time: Length of each frame of the animation | ||||||
|  |         :param momentum: Direction to move this effect | ||||||
|  |         """ | ||||||
|  |         self.start_time = game.alpha | ||||||
|  |         self.frame_time = frame_time | ||||||
|  |         self.images = images | ||||||
|  |         self.frame_start = game.alpha | ||||||
|  |         super().__init__(game, images[0], duration=len( | ||||||
|  |             images)*frame_time, momentum=momentum) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Animate the effect""" | ||||||
|  |         super().tick() | ||||||
|  | 
 | ||||||
|  |         alpha = self.game.alpha - self.start_time | ||||||
|  | 
 | ||||||
|  |         i = int(alpha // self.frame_time) | ||||||
|  |         if i < len(self.images): | ||||||
|  |             self.set_image(self.images[i]) | ||||||
|  |         else: | ||||||
|  |             self.destroy() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EffectPlayer: | ||||||
|  |     """An object which concerns itself with managing the effects""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game) -> None: | ||||||
|  |         """Initialise the | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         """ | ||||||
|  |         self.sprites = [] | ||||||
|  |         self.game = game | ||||||
|  |         self.explosion_frames = [] | ||||||
|  |         self.star_image: PhotoImage | ||||||
|  | 
 | ||||||
|  |     def load_textures(self): | ||||||
|  |         """Load effect textures""" | ||||||
|  | 
 | ||||||
|  |         self.explosion_frames = [ | ||||||
|  |             self.game.texture_factory.get_image(f"explosion{i+1}") | ||||||
|  |             for i in range(3) | ||||||
|  |         ] | ||||||
|  |         self.star_image = self.game.texture_factory.get_image("star") | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update all effects""" | ||||||
|  |         for sprite in self.sprites: | ||||||
|  |             sprite.tick() | ||||||
|  | 
 | ||||||
|  |         self.sprites = Sprite.remove_destroyed(self.sprites) | ||||||
|  | 
 | ||||||
|  |     def create_stars(self): | ||||||
|  |         """Initialise the stars in the background""" | ||||||
|  |         for _ in range(100): | ||||||
|  |             self.create_star(True) | ||||||
|  | 
 | ||||||
|  |     def create_star(self, new=False): | ||||||
|  |         """Add a star to the background | ||||||
|  | 
 | ||||||
|  |         :param new: Whether this star should be added at | ||||||
|  |                     the top of the screen or anywhere | ||||||
|  |         """ | ||||||
|  |         x = randint(0, self.game.w) | ||||||
|  |         if new: | ||||||
|  |             y = randint(0, self.game.h) | ||||||
|  |         else: | ||||||
|  |             y = -1 | ||||||
|  | 
 | ||||||
|  |         speed = randint(1, 4) * 0.1 | ||||||
|  |         duration = 2*self.game.h / speed | ||||||
|  | 
 | ||||||
|  |         star = GameEffect( | ||||||
|  |             self.game, | ||||||
|  |             self.star_image, | ||||||
|  |             duration=int(duration), | ||||||
|  |             momentum=(0, speed) | ||||||
|  |         ) | ||||||
|  |         star.set_pos((x, y)) | ||||||
|  | 
 | ||||||
|  |         star.send_to_back() | ||||||
|  |         star.show() | ||||||
|  | 
 | ||||||
|  |         self.sprites.append(star) | ||||||
|  | 
 | ||||||
|  |     def create_explosion(self, position=(0, 0)): | ||||||
|  |         """Create an explosion effect | ||||||
|  | 
 | ||||||
|  |         :param position: location of the explosion | ||||||
|  |         """ | ||||||
|  |         for _ in range(randint(1, 3)): | ||||||
|  |             m = ((random()*2)-1, (random()*2)-1) | ||||||
|  |             explosion_sprite = AnimatedEffect( | ||||||
|  |                 self.game, self.explosion_frames, frame_time=5, momentum=m) | ||||||
|  |             explosion_sprite.set_pos(position) | ||||||
|  |             explosion_sprite.show() | ||||||
|  | 
 | ||||||
|  |             self.sprites.append(explosion_sprite) | ||||||
|  | 
 | ||||||
|  |     def splash_text(self, text, duration=50): | ||||||
|  |         """splash_text. | ||||||
|  | 
 | ||||||
|  |         :param text: | ||||||
|  |         :param duration: | ||||||
|  |         """ | ||||||
|  |         text_img = Font.load_text(self.game.texture_factory, text) | ||||||
|  |         position = ( | ||||||
|  |             (self.game.w-Font.FONT_WIDTH*len(text)) // 2, | ||||||
|  |             (self.game.h-Font.FONT_SIZE) // 3 | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         text_sprite = GameEffect( | ||||||
|  |             self.game, text_img, duration=duration) | ||||||
|  |         text_sprite.set_pos(position) | ||||||
|  |         text_sprite.show() | ||||||
|  |         self.sprites.append(text_sprite) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DamageableSprite(GameSprite): | ||||||
|  |     """Sprite with health points """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image_name: str, hp=3): | ||||||
|  |         """Initialise the sprite | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for this sprite | ||||||
|  |         :type image_name: str | ||||||
|  |         :param hp: The number of hit points this sprite spawns with | ||||||
|  |         """ | ||||||
|  |         self.image = game.texture_factory.get_image(image_name) | ||||||
|  |         self.white_image = game.texture_factory.get_image( | ||||||
|  |             f"{image_name}:white") | ||||||
|  | 
 | ||||||
|  |         self.hp = hp | ||||||
|  |         self.animation_frame = 0 | ||||||
|  | 
 | ||||||
|  |         super().__init__(game, self.image) | ||||||
|  | 
 | ||||||
|  |     def damage(self, amount=1): | ||||||
|  |         """Decrease number of hit points by an amount | ||||||
|  | 
 | ||||||
|  |         :param amount: | ||||||
|  |         """ | ||||||
|  |         if not self.destroyed: | ||||||
|  |             self.hp -= amount | ||||||
|  |             self.animation_frame = 5 | ||||||
|  |             if self.hp <= 0: | ||||||
|  |                 self.hp = 0 | ||||||
|  |                 self.destroy() | ||||||
|  |                 self.game.effect_player.create_explosion(self.get_pos()) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update the sprite""" | ||||||
|  |         super().tick() | ||||||
|  |         if self.animation_frame > 0: | ||||||
|  |             self.animation_frame -= 1 | ||||||
|  |             if self.animation_frame % 2 == 0: | ||||||
|  |                 self.set_image(self.image) | ||||||
|  |             else: | ||||||
|  |                 self.set_image(self.white_image) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     print("!!!") | ||||||
|  |     print("This is not the main file!") | ||||||
|  |     print("Pleae run\n\tpython main.py\ninstead!") | ||||||
|  |     print("!!!") | ||||||
							
								
								
									
										152
									
								
								hud.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								hud.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,152 @@ | ||||||
|  | from game import Game, GameSprite | ||||||
|  | from font import Font | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ScoreCounterSprite(GameSprite): | ||||||
|  |     """Single digit for a score counter""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game): | ||||||
|  |         """Initialise the score counter | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         """ | ||||||
|  |         self.number_images = [] | ||||||
|  | 
 | ||||||
|  |         self.x = 0 | ||||||
|  | 
 | ||||||
|  |         for i in range(10): | ||||||
|  |             self.number_images.append( | ||||||
|  |                 Font.load_text(game.texture_factory, str(i))) | ||||||
|  | 
 | ||||||
|  |         super().__init__(game, self.number_images[0]) | ||||||
|  | 
 | ||||||
|  |     def update_image(self): | ||||||
|  |         """Update the digit""" | ||||||
|  |         self.set_image(self.number_images[int(self.x % 10)]) | ||||||
|  | 
 | ||||||
|  |     def set(self, x): | ||||||
|  |         """Set the image | ||||||
|  | 
 | ||||||
|  |         :param x: number to set this digit to | ||||||
|  |         """ | ||||||
|  |         self.x = x | ||||||
|  |         self.update_image() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ScoreCounter: | ||||||
|  |     """Sprite to display a number""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, num_digits, position=(0, 0)) -> None: | ||||||
|  |         """__init__. | ||||||
|  | 
 | ||||||
|  |         :param game: | ||||||
|  |         :type game: Game | ||||||
|  |         :param num_digits: | ||||||
|  |         :param position: | ||||||
|  |         :rtype: None | ||||||
|  |         """ | ||||||
|  |         self.digits = [] | ||||||
|  |         x, y = position | ||||||
|  | 
 | ||||||
|  |         self.number = 0 | ||||||
|  | 
 | ||||||
|  |         for i in range(num_digits): | ||||||
|  |             sprite = ScoreCounterSprite(game) | ||||||
|  |             sprite.set_pos((x+(Font.FONT_SIZE + 1)*i, y)) | ||||||
|  |             self.digits.append(sprite) | ||||||
|  | 
 | ||||||
|  |     def set(self, number): | ||||||
|  |         """Set the score to be displayed | ||||||
|  | 
 | ||||||
|  |         :param number: | ||||||
|  |         """ | ||||||
|  |         if number != self.number: | ||||||
|  |             self.number = number | ||||||
|  |             power = 10**len(self.digits) | ||||||
|  |             for digit in self.digits: | ||||||
|  |                 power /= 10 | ||||||
|  |                 digit.set((number // power) % 10) | ||||||
|  | 
 | ||||||
|  |     def destroy(self): | ||||||
|  |         """Remove this counter""" | ||||||
|  |         for n in self.digits: | ||||||
|  |             n.destroy() | ||||||
|  | 
 | ||||||
|  |     def send_to_front(self): | ||||||
|  |         """Move this counter to the foreground""" | ||||||
|  |         for d in self.digits: | ||||||
|  |             d.send_to_front() | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Make this counter visible""" | ||||||
|  |         for d in self.digits: | ||||||
|  |             d.show() | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Make this counter invisible""" | ||||||
|  |         for d in self.digits: | ||||||
|  |             d.hide() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class GameHud: | ||||||
|  |     """Object to manage the items visible in the game's heads up display""" | ||||||
|  | 
 | ||||||
|  |     SCORE_DIGITS = 8 | ||||||
|  |     HP_DIGITS = 2 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game) -> None: | ||||||
|  |         """Initialise the HUD | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         self.game = game | ||||||
|  |         self.score_counter = ScoreCounter(game, GameHud.SCORE_DIGITS, | ||||||
|  |                                           position=( | ||||||
|  |                                               game.w | ||||||
|  |                                               - GameHud.SCORE_DIGITS | ||||||
|  |                                               * (Font.FONT_SIZE+1), | ||||||
|  |                                               1) | ||||||
|  |                                           ) | ||||||
|  | 
 | ||||||
|  |         self.hp_symbol = GameSprite(game, game.player.image) | ||||||
|  |         self.hp_symbol.set_pos((1, 1)) | ||||||
|  | 
 | ||||||
|  |         x_image = Font.load_text(game.texture_factory, "x") | ||||||
|  |         self.x_symbol = GameSprite(game, x_image) | ||||||
|  |         self.x_symbol.set_pos((self.hp_symbol.x+self.hp_symbol.w+1, 1)) | ||||||
|  | 
 | ||||||
|  |         self.hp_counter = ScoreCounter(game, GameHud.HP_DIGITS, | ||||||
|  |                                        position=(self.x_symbol.x+1 + | ||||||
|  |                                                  self.x_symbol.w, 1) | ||||||
|  |                                        ) | ||||||
|  | 
 | ||||||
|  |         self.items = (self.score_counter, | ||||||
|  |                       self.hp_symbol, | ||||||
|  |                       self.x_symbol, | ||||||
|  |                       self.hp_counter) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update the hud""" | ||||||
|  |         self.score_counter.set(self.game.score) | ||||||
|  |         self.hp_counter.set( | ||||||
|  |             self.game.player.hp if self.game.player.hp > 0 else 0) | ||||||
|  | 
 | ||||||
|  |         for x in self.items: | ||||||
|  |             x.send_to_front() | ||||||
|  | 
 | ||||||
|  |     def destroy(self): | ||||||
|  |         """Remove all the associated objects""" | ||||||
|  |         for x in self.items: | ||||||
|  |             x.destroy() | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Make this object invisible""" | ||||||
|  |         for x in self.items: | ||||||
|  |             x.hide() | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Make this object visible""" | ||||||
|  |         for x in self.items: | ||||||
|  |             x.show() | ||||||
							
								
								
									
										133
									
								
								inputs.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								inputs.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,133 @@ | ||||||
|  | from dataclasses import dataclass | ||||||
|  | from os import path | ||||||
|  | from sys import stderr | ||||||
|  | 
 | ||||||
|  | from config import Config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class InputSettings: | ||||||
|  |     """Settings for keybinds""" | ||||||
|  | 
 | ||||||
|  |     down: str = "Down" | ||||||
|  |     left: str = "Left" | ||||||
|  |     up: str = "Up" | ||||||
|  |     right: str = "Right" | ||||||
|  |     action: str = "space" | ||||||
|  |     pause: str = "Escape" | ||||||
|  |     boss: str = "F9" | ||||||
|  | 
 | ||||||
|  |     def save_inputs(self): | ||||||
|  |         """Save keybinds to a file""" | ||||||
|  |         with open(Config.SETTINGS_FILE, "w", encoding="utf-8") as file: | ||||||
|  |             for key, value in vars(self).items(): | ||||||
|  |                 file.write(f"{key}: {value}\n") | ||||||
|  | 
 | ||||||
|  |     def load_inputs(self): | ||||||
|  |         """Load keybinds from a file""" | ||||||
|  |         if path.exists(Config.SETTINGS_FILE): | ||||||
|  |             with open(Config.SETTINGS_FILE, "r", encoding="utf-8") as file: | ||||||
|  |                 for line in file.readlines(): | ||||||
|  |                     split = line.strip().split(": ") | ||||||
|  |                     if len(split) == 2: | ||||||
|  |                         setattr(self, split[0], split[1]) | ||||||
|  |                     else: | ||||||
|  |                         print( | ||||||
|  |                             f"Settings file corrupted? Invalid line {line}", | ||||||
|  |                             file=stderr | ||||||
|  |                             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class InputController: | ||||||
|  |     """Object which listens to key inputs""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game) -> None: | ||||||
|  |         """Initialise the input controller | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :rtype: None | ||||||
|  |         """ | ||||||
|  |         game.win.bind('<KeyPress>', self.on_key_press) | ||||||
|  |         game.win.bind('<KeyRelease>', self.on_key_release) | ||||||
|  | 
 | ||||||
|  |         self.handlers = [] | ||||||
|  | 
 | ||||||
|  |         self.settings = InputSettings() | ||||||
|  |         self.settings.load_inputs() | ||||||
|  | 
 | ||||||
|  |         self.k_down = False | ||||||
|  |         self.k_left = False | ||||||
|  |         self.k_up = False | ||||||
|  |         self.k_right = False | ||||||
|  |         self.k_action = False | ||||||
|  |         self.k_pause = False | ||||||
|  |         self.k_boss = False | ||||||
|  | 
 | ||||||
|  |     def on_key_press(self, e): | ||||||
|  |         """Handle Key press events | ||||||
|  | 
 | ||||||
|  |         :param e: The key press event to handle | ||||||
|  |         """ | ||||||
|  |         if e.keysym == self.settings.left: | ||||||
|  |             self.k_left = True | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.right: | ||||||
|  |             self.k_right = True | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.up: | ||||||
|  |             self.k_up = True | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.down: | ||||||
|  |             self.k_down = True | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.action: | ||||||
|  |             self.k_action = True | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.pause: | ||||||
|  |             self.k_pause = True | ||||||
|  | 
 | ||||||
|  |         for t, h in self.handlers: | ||||||
|  |             if t == "press" and h(e): | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |     def on_key_release(self, e): | ||||||
|  |         """Handle Key release events | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         :param e: The key press event to handle | ||||||
|  |         """ | ||||||
|  |         if e.keysym == self.settings.left: | ||||||
|  |             self.k_left = False | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.right: | ||||||
|  |             self.k_right = False | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.up: | ||||||
|  |             self.k_up = False | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.down: | ||||||
|  |             self.k_down = False | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.action: | ||||||
|  |             self.k_action = False | ||||||
|  | 
 | ||||||
|  |         if e.keysym == self.settings.pause: | ||||||
|  |             self.k_pause = False | ||||||
|  | 
 | ||||||
|  |         for t, h in self.handlers: | ||||||
|  |             if t == "release" and h(e): | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |     def add_keypress_handler(self, callback): | ||||||
|  |         """Register a key press listener | ||||||
|  | 
 | ||||||
|  |         :param callback: | ||||||
|  |         """ | ||||||
|  |         self.handlers.insert(0, ("press", callback)) | ||||||
|  | 
 | ||||||
|  |     def add_keyrelease_handler(self, callback): | ||||||
|  |         """Register a key release listener | ||||||
|  | 
 | ||||||
|  |         :param callback: | ||||||
|  |         """ | ||||||
|  |         self.handlers.insert(0, ("release", callback)) | ||||||
							
								
								
									
										443
									
								
								leaderboard.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										443
									
								
								leaderboard.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,443 @@ | ||||||
|  | from os import path | ||||||
|  | from random import randint | ||||||
|  | from typing import List | ||||||
|  | 
 | ||||||
|  | from config import Config | ||||||
|  | from font import Font | ||||||
|  | from game import Game, GameSprite | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class LeaderboardFile: | ||||||
|  |     """Object to manage saving and loading the leaderboard""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         """Initialise the leaderboard file""" | ||||||
|  |         self.entries = [] | ||||||
|  | 
 | ||||||
|  |     def load_entries(self): | ||||||
|  |         """Load leaderboard entries from a file""" | ||||||
|  |         self.entries = [] | ||||||
|  |         if path.exists(Config.LEADERBOARD_FILE): | ||||||
|  |             with open(Config.LEADERBOARD_FILE, "rb") as file: | ||||||
|  |                 while (entry := file.read(8 + Config.NICK_LEN)): | ||||||
|  |                     name = entry[0:Config.NICK_LEN] | ||||||
|  |                     score = entry[Config.NICK_LEN:11] | ||||||
|  |                     score_entry = (name.decode("ascii"), | ||||||
|  |                                    int.from_bytes(score, byteorder="little")) | ||||||
|  |                     self.entries.append(score_entry) | ||||||
|  |             self.sort_entries() | ||||||
|  | 
 | ||||||
|  |     def sort_entries(self): | ||||||
|  |         """Sort leaderboard entries""" | ||||||
|  |         self.entries.sort(key=(lambda e: e[1])) | ||||||
|  |         self.entries.reverse() | ||||||
|  | 
 | ||||||
|  |     def save_entries(self): | ||||||
|  |         """Save leaderboard entries""" | ||||||
|  |         with open(Config.LEADERBOARD_FILE, "wb") as file: | ||||||
|  |             for name, score in self.entries: | ||||||
|  |                 file.write(bytes(name[0:Config.NICK_LEN], "ascii")) | ||||||
|  |                 file.write(int(score).to_bytes(8, "little")) | ||||||
|  | 
 | ||||||
|  |     def add_entry(self, name, score): | ||||||
|  |         """Add a leaderboard entry | ||||||
|  | 
 | ||||||
|  |         :param name: Initials of the player | ||||||
|  |         :param score: The sore that was achieved | ||||||
|  |         """ | ||||||
|  |         self.entries.append((name, score)) | ||||||
|  |         self.sort_entries() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NameEntryLetter(GameSprite): | ||||||
|  |     """A single sprite used in a initial entry""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, image, letter): | ||||||
|  |         """Initialise the letter | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image: The image to use for this | ||||||
|  |         :param letter: the letter to use | ||||||
|  |         """ | ||||||
|  |         self.letter = letter | ||||||
|  |         super().__init__(game, image) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NameEntry(): | ||||||
|  |     """An initial entry element, allowing the user to enter their initials""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, callback, num_letters=3, position=(0, 0)): | ||||||
|  |         """Initialise the name entry | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param callback: callback to call when the entry is complete | ||||||
|  |         :param num_letters: Number of letters to use for the initials | ||||||
|  |         :param position: Position of this element | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.callback = callback | ||||||
|  | 
 | ||||||
|  |         self.alphabet = [ | ||||||
|  |             Font.load_text(game.texture_factory, c) | ||||||
|  |             for c in list(map(chr, range(97, 123))) | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |         self.letters: List[NameEntryLetter] = [] | ||||||
|  |         self.selection = 0 | ||||||
|  | 
 | ||||||
|  |         self.hidden = True | ||||||
|  | 
 | ||||||
|  |         self.populate_letters(num_letters) | ||||||
|  |         self.game.inputs.add_keypress_handler(self.on_key) | ||||||
|  |         self.set_pos(position) | ||||||
|  | 
 | ||||||
|  |     def populate_letters(self, num_letters): | ||||||
|  |         """Create sprites for each of the letters | ||||||
|  | 
 | ||||||
|  |         :param num_letters: Number of letters to use for initials | ||||||
|  |         """ | ||||||
|  |         for _ in range(num_letters): | ||||||
|  |             sprite = NameEntryLetter( | ||||||
|  |                 self.game,  self.alphabet[0], 0 | ||||||
|  |             ) | ||||||
|  |             self.letters.append(sprite) | ||||||
|  | 
 | ||||||
|  |         enter_image = Font.load_text(self.game.texture_factory, "enter") | ||||||
|  |         self.button = GameSprite(self.game, enter_image) | ||||||
|  |         self.w = self.button.w + (self.letters[0].w+1)*len(self.letters) | ||||||
|  |         self.h = Font.FONT_SIZE | ||||||
|  | 
 | ||||||
|  |     def on_key(self, _): | ||||||
|  |         """Handle Key press events | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         :param _: The key press event to handle | ||||||
|  |         """ | ||||||
|  |         inp = self.game.inputs | ||||||
|  |         if not self.hidden: | ||||||
|  |             if inp.k_action and self.selection == len(self.letters): | ||||||
|  |                 self.callback(self.get_string()) | ||||||
|  |                 return True | ||||||
|  | 
 | ||||||
|  |             if inp.k_left: | ||||||
|  |                 self.selection -= 1 | ||||||
|  |                 self.get_selected_letter().show() | ||||||
|  | 
 | ||||||
|  |             if inp.k_right or inp.k_action: | ||||||
|  |                 self.get_selected_letter().show() | ||||||
|  |                 self.selection += 1 | ||||||
|  |                 self.get_selected_letter().hide() | ||||||
|  | 
 | ||||||
|  |             self.selection %= len(self.letters) + 1 | ||||||
|  | 
 | ||||||
|  |             if inp.k_up: | ||||||
|  |                 self.modify_letter(-1) | ||||||
|  | 
 | ||||||
|  |             if inp.k_down: | ||||||
|  |                 self.modify_letter(1) | ||||||
|  | 
 | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def get_string(self): | ||||||
|  |         """Get the initials entered""" | ||||||
|  |         return "".join(map(lambda l: chr(97 + l.letter), self.letters)) | ||||||
|  | 
 | ||||||
|  |     def modify_letter(self, amount): | ||||||
|  |         """Increase or decrease a single character | ||||||
|  | 
 | ||||||
|  |         :param amount: number of letters to increment by | ||||||
|  |         """ | ||||||
|  |         letter = self.get_selected_letter() | ||||||
|  |         if letter in self.letters: | ||||||
|  |             letter.letter += amount | ||||||
|  |             letter.letter %= len(self.alphabet) | ||||||
|  |             self.update_letter(letter) | ||||||
|  |             letter.show() | ||||||
|  | 
 | ||||||
|  |     def update_letter(self, letter): | ||||||
|  |         """Upare the image of a single letter | ||||||
|  | 
 | ||||||
|  |         :param letter: letter to update | ||||||
|  |         """ | ||||||
|  |         letter.set_image(self.alphabet[letter.letter]) | ||||||
|  | 
 | ||||||
|  |     def get_selected_letter(self): | ||||||
|  |         """Get the letter that has been selected""" | ||||||
|  |         if self.selection < len(self.letters): | ||||||
|  |             return self.letters[self.selection] | ||||||
|  | 
 | ||||||
|  |         return self.button | ||||||
|  | 
 | ||||||
|  |     def set_pos(self, pos): | ||||||
|  |         """set the element's position | ||||||
|  | 
 | ||||||
|  |         :param pos: position to move to | ||||||
|  |         """ | ||||||
|  |         pos_x, pos_y = pos | ||||||
|  |         offset_x = 0 | ||||||
|  |         for letter in self.letters: | ||||||
|  |             letter.set_pos((pos_x + offset_x, pos_y)) | ||||||
|  |             offset_x += letter.w + 1 | ||||||
|  |         offset_x += Font.FONT_SIZE | ||||||
|  | 
 | ||||||
|  |         self.button.set_pos((pos_x + offset_x, pos_y)) | ||||||
|  | 
 | ||||||
|  |     def update_position(self): | ||||||
|  |         """Update the position of all the letters""" | ||||||
|  |         for letter in self.letters: | ||||||
|  |             letter.update_position() | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Make this object visible""" | ||||||
|  |         if self.hidden: | ||||||
|  |             for letter in self.letters: | ||||||
|  |                 letter.show() | ||||||
|  |                 letter.send_to_front() | ||||||
|  |             self.button.show() | ||||||
|  |             self.button.send_to_front() | ||||||
|  |             self.hidden = False | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Make this object invisible""" | ||||||
|  |         if not self.hidden: | ||||||
|  |             for letter in self.letters: | ||||||
|  |                 letter.hide() | ||||||
|  |             self.button.hide() | ||||||
|  |             self.hidden = True | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update the state of this object""" | ||||||
|  |         if not self.hidden: | ||||||
|  |             selected = self.get_selected_letter() | ||||||
|  |             for letter in self.letters + [self.button]: | ||||||
|  |                 if self.game.alpha//15 == self.game.alpha / 15: | ||||||
|  |                     if letter == selected: | ||||||
|  |                         if (self.game.alpha//15) % 2 == 0: | ||||||
|  |                             self.get_selected_letter().show() | ||||||
|  |                         else: | ||||||
|  |                             self.get_selected_letter().hide() | ||||||
|  |                     else: | ||||||
|  |                         letter.show() | ||||||
|  |         else: | ||||||
|  |             self.hide() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Leaderboard: | ||||||
|  |     """Leaderboard object to display previous scores""" | ||||||
|  | 
 | ||||||
|  |     ANIMATION_TIME = 5 | ||||||
|  |     ANIMATION_DELAY = 5 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game): | ||||||
|  |         """Initialise the leaderboard | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.file = LeaderboardFile() | ||||||
|  |         self.entries = [] | ||||||
|  |         self.editing = True | ||||||
|  |         self.padding = 5 | ||||||
|  | 
 | ||||||
|  |         self.callback = (lambda: None) | ||||||
|  | 
 | ||||||
|  |         self.hidden = True | ||||||
|  | 
 | ||||||
|  |         self.game.inputs.add_keypress_handler(self.on_key) | ||||||
|  |         self.name_entry = NameEntry(self.game, self.submit_name) | ||||||
|  | 
 | ||||||
|  |         self.blinking_sprite = None | ||||||
|  |         self.animation_start = -1 | ||||||
|  | 
 | ||||||
|  |     def populate_entries(self, blink_entry=("", 0)): | ||||||
|  |         """Populate entries. | ||||||
|  | 
 | ||||||
|  |         :param blink_entry: | ||||||
|  |         """ | ||||||
|  |         self.clear_entries() | ||||||
|  |         self.file.load_entries() | ||||||
|  | 
 | ||||||
|  |         editing_area = 0 | ||||||
|  |         if self.editing: | ||||||
|  |             editing_area = Font.FONT_SIZE + self.padding*2 | ||||||
|  |         remaining_area = self.game.h - self.padding*2 - editing_area | ||||||
|  | 
 | ||||||
|  |         to_fit = remaining_area // (Font.FONT_SIZE+self.padding) - 1 | ||||||
|  | 
 | ||||||
|  |         to_draw = self.file.entries[0:to_fit] | ||||||
|  | 
 | ||||||
|  |         # create a row variable that is incremented for each entry | ||||||
|  |         y = self.padding | ||||||
|  | 
 | ||||||
|  |         # create the title sprite and increment the row | ||||||
|  |         image = Font.load_text(self.game.texture_factory, "leaderboard") | ||||||
|  |         sprite = GameSprite(self.game, image) | ||||||
|  |         sprite.set_pos((0, y)) | ||||||
|  |         self.entries.append(sprite) | ||||||
|  |         x = (self.game.w - sprite.w) // 2 | ||||||
|  |         sprite.set_pos((x, y)) | ||||||
|  | 
 | ||||||
|  |         y += sprite.h + self.padding | ||||||
|  | 
 | ||||||
|  |         # calculate the number of zeros to pad the score by | ||||||
|  |         zfill = ((self.game.w-self.padding*2) // | ||||||
|  |                  (Font.FONT_SIZE+1)) - Config.NICK_LEN - 5 | ||||||
|  |         for name, score in to_draw: | ||||||
|  |             text = f"{name}     {str(score).zfill(zfill)}" | ||||||
|  |             x = self.padding | ||||||
|  |             image = Font.load_text(self.game.texture_factory, text) | ||||||
|  |             sprite = GameSprite(self.game, image) | ||||||
|  |             sprite.set_pos((x, y)) | ||||||
|  | 
 | ||||||
|  |             if (name, score) == blink_entry: | ||||||
|  |                 self.blinking_sprite = sprite | ||||||
|  |             self.entries.append(sprite) | ||||||
|  | 
 | ||||||
|  |             y += sprite.h + self.padding | ||||||
|  | 
 | ||||||
|  |         if self.editing: | ||||||
|  |             self.name_entry.set_pos((self.padding, y+self.padding)) | ||||||
|  |             self.name_entry.show() | ||||||
|  |         else: | ||||||
|  |             self.name_entry.hide() | ||||||
|  | 
 | ||||||
|  |     def start_animation(self): | ||||||
|  |         """Start the animation.""" | ||||||
|  |         for e in self.entries: | ||||||
|  |             e.set_pos((-self.game.w, e.y)) | ||||||
|  |         self.name_entry.hide() | ||||||
|  |         self.animation_start = self.game.alpha | ||||||
|  | 
 | ||||||
|  |     def on_key(self, _): | ||||||
|  |         """Handle Key press events | ||||||
|  | 
 | ||||||
|  |         :param _: The key press event to handle | ||||||
|  |         """ | ||||||
|  |         inp = self.game.inputs | ||||||
|  |         if not self.hidden and inp.k_action and not self.editing: | ||||||
|  |             self.callback() | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def submit_name(self, name): | ||||||
|  |         """Submit a name to the leaderboard | ||||||
|  | 
 | ||||||
|  |         :param name: | ||||||
|  |         """ | ||||||
|  |         score = self.game.score | ||||||
|  |         self.file.add_entry(name, score) | ||||||
|  |         self.file.save_entries() | ||||||
|  | 
 | ||||||
|  |         self.editing = False | ||||||
|  |         self.name_entry.hide() | ||||||
|  | 
 | ||||||
|  |         self.populate_entries(blink_entry=(name, score)) | ||||||
|  |         self.start_animation() | ||||||
|  |         for e in self.entries: | ||||||
|  |             e.show() | ||||||
|  | 
 | ||||||
|  |     def animate_sprite(self, sprite, i): | ||||||
|  |         """Animate a single sprite. | ||||||
|  | 
 | ||||||
|  |         :param sprite: | ||||||
|  |         :param i: | ||||||
|  |         """ | ||||||
|  |         alpha = self.game.alpha \ | ||||||
|  |             - self.animation_start \ | ||||||
|  |             - i*Leaderboard.ANIMATION_DELAY | ||||||
|  | 
 | ||||||
|  |         if alpha <= Leaderboard.ANIMATION_TIME: | ||||||
|  |             if i == 0: | ||||||
|  |                 # only title should be h aligned | ||||||
|  |                 cx = (self.game.w - sprite.w) // 2 | ||||||
|  |             else: | ||||||
|  |                 cx = self.padding | ||||||
|  | 
 | ||||||
|  |             x = (alpha/Leaderboard.ANIMATION_TIME) * \ | ||||||
|  |                 (cx+self.game.w//2) - self.game.w//2 | ||||||
|  |             sprite.set_pos((x, sprite.y)) | ||||||
|  | 
 | ||||||
|  |             return False | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update the leaderboard""" | ||||||
|  |         animation_complete = True | ||||||
|  |         for i, sprite in enumerate(self.entries): | ||||||
|  |             sprite.send_to_front() | ||||||
|  |             if not self.animate_sprite(sprite, i): | ||||||
|  |                 animation_complete = False | ||||||
|  | 
 | ||||||
|  |         if self.editing: | ||||||
|  |             animation_time = self.game.alpha \ | ||||||
|  |                 - self.animation_start \ | ||||||
|  |                 - len(self.entries)*Leaderboard.ANIMATION_DELAY | ||||||
|  | 
 | ||||||
|  |             if animation_complete \ | ||||||
|  |                     and animation_time > Leaderboard.ANIMATION_TIME: | ||||||
|  |                 self.name_entry.show() | ||||||
|  |                 self.name_entry.tick() | ||||||
|  |         else: | ||||||
|  |             if self.blinking_sprite is not None: | ||||||
|  |                 if (self.game.alpha//15) % 2 == 0: | ||||||
|  |                     self.blinking_sprite.show() | ||||||
|  |                 else: | ||||||
|  |                     self.blinking_sprite.hide() | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Make this object visible""" | ||||||
|  |         if self.hidden: | ||||||
|  |             for m in self.entries: | ||||||
|  |                 m.show() | ||||||
|  |                 m.send_to_front() | ||||||
|  | 
 | ||||||
|  |             self.hidden = False | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Make this object invisible""" | ||||||
|  |         if not self.hidden: | ||||||
|  |             for m in self.entries: | ||||||
|  |                 m.hide() | ||||||
|  |             self.name_entry.hide() | ||||||
|  |             self.hidden = True | ||||||
|  | 
 | ||||||
|  |     def start_editing(self): | ||||||
|  |         """Allow the user to input a name""" | ||||||
|  |         self.editing = True | ||||||
|  |         self.blinking_sprite = None | ||||||
|  | 
 | ||||||
|  |     def clear_entries(self): | ||||||
|  |         """Remove all the associated objects""" | ||||||
|  |         for entry in self.entries: | ||||||
|  |             entry.destroy() | ||||||
|  |         self.entries = [] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # test to add entries to game leaderboard | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     lb = LeaderboardFile() | ||||||
|  |     lb.load_entries() | ||||||
|  | 
 | ||||||
|  |     for input_name, input_score in lb.entries: | ||||||
|  |         print(f"{input_name} {input_score}") | ||||||
|  | 
 | ||||||
|  |     while True: | ||||||
|  |         input_name = input("Enter name or leave blank to exit: ") | ||||||
|  |         if input_name: | ||||||
|  |             input_score = input("enter score blank for random: ") | ||||||
|  |             if not input_score: | ||||||
|  |                 input_score = randint(1, 999999) | ||||||
|  |             lb.add_entry(input_name, int(input_score)) | ||||||
|  |         else: | ||||||
|  |             break | ||||||
|  | 
 | ||||||
|  |     for input_name, input_score in lb.entries: | ||||||
|  |         print(f"{input_name} {input_score}") | ||||||
|  | 
 | ||||||
|  |     lb.save_entries() | ||||||
							
								
								
									
										13
									
								
								main.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								main.py
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | #!/usr/bin/env python3 | ||||||
|  | 
 | ||||||
|  | from shooter_game import ShooterGame | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     """The entry function to the game""" | ||||||
|  |     game = ShooterGame() | ||||||
|  |     game.start() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										282
									
								
								menu.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								menu.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,282 @@ | ||||||
|  | import re | ||||||
|  | 
 | ||||||
|  | from font import Font | ||||||
|  | from game import Game, GameSprite | ||||||
|  | from sprite import Sprite | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MenuItem(GameSprite): | ||||||
|  |     """A selectable item in a menu""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, text, callback): | ||||||
|  |         """Initialise the item | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param text: Text to display for this item | ||||||
|  |         :param callback: function to call when this item is selected | ||||||
|  |         """ | ||||||
|  |         image = Font.load_text(game.texture_factory, text) | ||||||
|  |         self.text = text | ||||||
|  |         self.callback = callback | ||||||
|  |         super().__init__(game, image) | ||||||
|  | 
 | ||||||
|  |     def set_text(self, text): | ||||||
|  |         """Update the text of an entry | ||||||
|  | 
 | ||||||
|  |         :param text: | ||||||
|  |         """ | ||||||
|  |         self.text = text | ||||||
|  |         image = Font.load_text(self.game.texture_factory, text) | ||||||
|  |         self.set_image(image) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Menu(): | ||||||
|  |     """Menu object with selectable entries""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, title) -> None: | ||||||
|  |         """Initialise the menu object | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param title: The title of this menu | ||||||
|  |         """ | ||||||
|  |         self.game = game | ||||||
|  |         self.title = title | ||||||
|  |         self.padding = 5 | ||||||
|  | 
 | ||||||
|  |         self.game.inputs.add_keypress_handler(self.on_key) | ||||||
|  | 
 | ||||||
|  |         self.menu_items = [] | ||||||
|  | 
 | ||||||
|  |         self.selection = 0 | ||||||
|  | 
 | ||||||
|  |         carret_image = Font.load_text(game.texture_factory, ">") | ||||||
|  |         self.carret = GameSprite(self.game, carret_image) | ||||||
|  | 
 | ||||||
|  |         title_image = Font.load_text(game.texture_factory, title) | ||||||
|  | 
 | ||||||
|  |         position = ((self.game.w - len(title)*Font.FONT_WIDTH)//2, 5*2) | ||||||
|  |         self.title = GameSprite(self.game, title_image) | ||||||
|  |         self.title.set_pos(position) | ||||||
|  | 
 | ||||||
|  |         self.hidden = True | ||||||
|  | 
 | ||||||
|  |         self.alpha = 0 | ||||||
|  | 
 | ||||||
|  |     def on_key(self, _): | ||||||
|  |         """Handle Key press events | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         :param event: The key press event to handle | ||||||
|  |         """ | ||||||
|  |         if not self.hidden: | ||||||
|  |             inp = self.game.inputs | ||||||
|  | 
 | ||||||
|  |             if inp.k_down or inp.k_right: | ||||||
|  |                 self.selection += 1 | ||||||
|  | 
 | ||||||
|  |             if inp.k_up or inp.k_left: | ||||||
|  |                 self.selection -= 1 | ||||||
|  | 
 | ||||||
|  |             self.selection %= len(self.menu_items) | ||||||
|  |             self.update_carret() | ||||||
|  | 
 | ||||||
|  |             if inp.k_action: | ||||||
|  |                 self.menu_items[self.selection].callback() | ||||||
|  | 
 | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def update_carret(self): | ||||||
|  |         """Move the carret to the correct location""" | ||||||
|  |         selected = self.menu_items[self.selection] | ||||||
|  |         x = selected.x - Font.FONT_SIZE | ||||||
|  |         y = selected.y | ||||||
|  |         self.carret.set_pos((x, y)) | ||||||
|  | 
 | ||||||
|  |     def arrange_items(self): | ||||||
|  |         """Move the menu items to their correct positions""" | ||||||
|  |         cy = self.game.h // 2 | ||||||
|  | 
 | ||||||
|  |         max_height = sum(item.h for item in self.menu_items | ||||||
|  |                          ) + self.padding*(len(self.menu_items)-1) | ||||||
|  |         top = cy - max_height//2 | ||||||
|  | 
 | ||||||
|  |         y = top | ||||||
|  | 
 | ||||||
|  |         for item in self.menu_items: | ||||||
|  |             item_x = (self.game.w - item.w) // 2 | ||||||
|  |             item_y = y | ||||||
|  |             y += item.h + self.padding | ||||||
|  |             item.set_pos((item_x, item_y)) | ||||||
|  |         self.update_carret() | ||||||
|  | 
 | ||||||
|  |     def add_item(self, text: str, callback, index=-1): | ||||||
|  |         """Add a menu item | ||||||
|  | 
 | ||||||
|  |         :param text: Label of this item | ||||||
|  |         :type text: str | ||||||
|  |         :param callback: Function to call when it is selected | ||||||
|  |         :param index: Where to insert this item | ||||||
|  |         """ | ||||||
|  |         if index == -1: | ||||||
|  |             index = len(self.menu_items) | ||||||
|  |         self.menu_items.insert(index, MenuItem(self.game, text, callback)) | ||||||
|  |         self.arrange_items() | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Make this object visible""" | ||||||
|  |         if self.hidden: | ||||||
|  |             for m in self.menu_items: | ||||||
|  |                 m.show() | ||||||
|  |                 m.send_to_front() | ||||||
|  |             self.carret.show() | ||||||
|  |             self.carret.send_to_front() | ||||||
|  | 
 | ||||||
|  |             self.title.show() | ||||||
|  |             self.title.send_to_front() | ||||||
|  | 
 | ||||||
|  |             self.hidden = False | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Make this object invisible""" | ||||||
|  |         if not self.hidden: | ||||||
|  |             for m in self.menu_items: | ||||||
|  |                 m.hide() | ||||||
|  |             self.carret.hide() | ||||||
|  | 
 | ||||||
|  |             self.title.hide() | ||||||
|  | 
 | ||||||
|  |             self.hidden = True | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update this object""" | ||||||
|  |         self.alpha += 1 | ||||||
|  |         if not self.hidden: | ||||||
|  |             if (self.alpha//15) % 2 == 0: | ||||||
|  |                 self.carret.show() | ||||||
|  |             else: | ||||||
|  |                 self.carret.hide() | ||||||
|  |         else: | ||||||
|  |             self.carret.hide() | ||||||
|  | 
 | ||||||
|  |     def has_item(self, text): | ||||||
|  |         """Return true if matching item is found | ||||||
|  | 
 | ||||||
|  |         :param text: Label text to match | ||||||
|  |         """ | ||||||
|  |         for entry in self.menu_items: | ||||||
|  |             if entry.text == text: | ||||||
|  |                 return True | ||||||
|  | 
 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def get_item(self, regex) -> MenuItem: | ||||||
|  |         """Return an item that is matched | ||||||
|  | 
 | ||||||
|  |         :param regex: regular expression to match item text to | ||||||
|  |         :rtype: MenuItem | ||||||
|  |         """ | ||||||
|  |         for entry in self.menu_items: | ||||||
|  |             if re.match(regex, entry.text): | ||||||
|  |                 return entry | ||||||
|  | 
 | ||||||
|  |         return self.menu_items[0] | ||||||
|  | 
 | ||||||
|  |     def edit_item(self, regex, new_text): | ||||||
|  |         """Edit the text of a menu item | ||||||
|  | 
 | ||||||
|  |         :param regex: Regular expression to use to match the item's text to | ||||||
|  |         :param new_text: Text to replace to | ||||||
|  |         """ | ||||||
|  |         for entry in self.menu_items: | ||||||
|  |             if re.match(regex, entry.text): | ||||||
|  |                 entry.set_text(new_text) | ||||||
|  | 
 | ||||||
|  |         self.arrange_items() | ||||||
|  | 
 | ||||||
|  |     def del_item(self, text): | ||||||
|  |         """Remove an item | ||||||
|  | 
 | ||||||
|  |         :param text: Label text to match | ||||||
|  |         """ | ||||||
|  |         for entry in self.menu_items: | ||||||
|  |             if entry.text == text: | ||||||
|  |                 entry.destroy() | ||||||
|  |         self.menu_items = Sprite.remove_destroyed(self.menu_items) | ||||||
|  |         self.arrange_items() | ||||||
|  | 
 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class KeybindsMenu(Menu): | ||||||
|  |     """A menu for selecting keybinds on""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, title): | ||||||
|  |         """Initialise the menu | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param title: The title of this menu | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, title) | ||||||
|  |         self.key_selecting = "" | ||||||
|  | 
 | ||||||
|  |         image = Font.load_text(game.texture_factory, "press any key") | ||||||
|  |         self.press_key_sprite = GameSprite(self.game, image) | ||||||
|  |         self.press_key_sprite.set_pos( | ||||||
|  |             ((self.game.w - self.press_key_sprite.w) // 2, self.game.h // 2)) | ||||||
|  | 
 | ||||||
|  |     def on_key(self, event): | ||||||
|  |         """Handle Key press events | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         :param event: The key press event to handle | ||||||
|  |         """ | ||||||
|  |         if self.key_selecting: | ||||||
|  |             key = event.keysym | ||||||
|  |             self.set_keybind(self.key_selecting, key) | ||||||
|  |             self.press_key_sprite.hide() | ||||||
|  |             self.show() | ||||||
|  |             return True | ||||||
|  |         return super().on_key(event) | ||||||
|  | 
 | ||||||
|  |     def set_keybind(self, name, key): | ||||||
|  |         """set_keybind. | ||||||
|  | 
 | ||||||
|  |         :param name: | ||||||
|  |         :param key: | ||||||
|  |         """ | ||||||
|  |         setattr(self.game.inputs.settings, self.key_selecting, key) | ||||||
|  |         self.game.inputs.settings.save_inputs() | ||||||
|  |         self.key_selecting = "" | ||||||
|  |         self.edit_item(f"{name}\\s*<.*>", self.get_label(name, key)) | ||||||
|  | 
 | ||||||
|  |     def select_keybind(self, keyname): | ||||||
|  |         """Allow the user to press a key to decide their keybind | ||||||
|  | 
 | ||||||
|  |         :param keyname: | ||||||
|  |         """ | ||||||
|  |         self.hide() | ||||||
|  |         self.press_key_sprite.show() | ||||||
|  |         self.key_selecting = keyname | ||||||
|  | 
 | ||||||
|  |     def get_set_keybind(self, keyname): | ||||||
|  |         """Return a function that sets the keybind of a particular keyname | ||||||
|  | 
 | ||||||
|  |         :param keyname: | ||||||
|  |         """ | ||||||
|  |         return lambda: self.select_keybind(keyname) | ||||||
|  | 
 | ||||||
|  |     def get_label(self, name, value): | ||||||
|  |         """Get a label for a keybind item | ||||||
|  | 
 | ||||||
|  |         :param name: | ||||||
|  |         :param value: | ||||||
|  |         """ | ||||||
|  |         available_width = (self.game.w - Font.FONT_SIZE*2) // Font.FONT_WIDTH | ||||||
|  |         num_spaces = available_width - (len(name) + len(value) + 2) - 1 | ||||||
|  |         spaces = " " * num_spaces | ||||||
|  |         return f"{name} {spaces}<{value}>" | ||||||
							
								
								
									
										88
									
								
								shooter.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								shooter.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | ||||||
|  | from dataclasses import dataclass | ||||||
|  | from typing import List | ||||||
|  | 
 | ||||||
|  | from game import DamageableSprite, Game, GameSprite | ||||||
|  | from sprite import Sprite | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Lazer(GameSprite): | ||||||
|  |     """Lazer object that is shot by a shooter""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, velocity=-4, color="white"): | ||||||
|  |         """Initialise the lazer | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param velocity: Velocity to move the lazer at | ||||||
|  |         :param color: name of the colour of the lazer | ||||||
|  |         """ | ||||||
|  |         self.velocity = velocity | ||||||
|  |         self.game = game | ||||||
|  |         super().__init__(game, game.texture_factory.get_image( | ||||||
|  |             f"lazer:{color}")) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update this object""" | ||||||
|  |         self.move(0, self.velocity) | ||||||
|  |         if self.y + self.h > self.game.h or self.y < 0: | ||||||
|  |             self.destroy() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class ShooterAttributes: | ||||||
|  |     """Attributes for a shooter object""" | ||||||
|  | 
 | ||||||
|  |     lazer_color: str = "white" | ||||||
|  |     cooldown: int = 40 | ||||||
|  |     velocity: int = 1 | ||||||
|  |     hp: int = 3 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Shooter(DamageableSprite): | ||||||
|  |     """A game object that is able to shoot lazers""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game, | ||||||
|  |                  image_name: str, attributes: ShooterAttributes): | ||||||
|  |         """Initialise the shooter | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         :param image_name: The name of the image to use for this sprite | ||||||
|  |         :type image_name: str | ||||||
|  |         :param attributes: The attributes to use for this object | ||||||
|  |         :type attributes: ShooterAttributes | ||||||
|  |         """ | ||||||
|  |         super().__init__(game, image_name, hp=attributes.hp) | ||||||
|  |         self.lazers: List[Lazer] = [] | ||||||
|  |         self.attributes = attributes | ||||||
|  |         self.last_shot = self.game.alpha | ||||||
|  | 
 | ||||||
|  |     def shoot(self): | ||||||
|  |         """Soot a lazer if possible""" | ||||||
|  |         next_shot = self.last_shot + self.attributes.cooldown | ||||||
|  | 
 | ||||||
|  |         if not self.destroyed \ | ||||||
|  |                 and self.game.alpha > next_shot: | ||||||
|  |             self.last_shot = self.game.alpha | ||||||
|  | 
 | ||||||
|  |             lazer = Lazer(self.game, | ||||||
|  |                           velocity=self.attributes.velocity, | ||||||
|  |                           color=self.attributes.lazer_color) | ||||||
|  |             lazer.set_pos((self.x + self.w//2 - 1, self.y + | ||||||
|  |                           self.h//2 - 1)) | ||||||
|  |             lazer.show() | ||||||
|  |             self.lazers.append(lazer) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update this object""" | ||||||
|  |         super().tick() | ||||||
|  |         for lazer in self.lazers: | ||||||
|  |             lazer.tick() | ||||||
|  | 
 | ||||||
|  |         self.lazers = Sprite.remove_destroyed(self.lazers) | ||||||
|  | 
 | ||||||
|  |     def destroy(self): | ||||||
|  |         """Remove all the associated objects""" | ||||||
|  |         super().destroy() | ||||||
|  |         for lazer in self.lazers: | ||||||
|  |             self.game.sprites.append(lazer) | ||||||
							
								
								
									
										363
									
								
								shooter_game.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								shooter_game.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,363 @@ | ||||||
|  | from enum import Enum, auto | ||||||
|  | from os import path, remove | ||||||
|  | from random import random | ||||||
|  | 
 | ||||||
|  | from boss_key import BossKey | ||||||
|  | from cheat_engine import Cheat, CheatEngine, DevModeCheat, InvincibilityCheat | ||||||
|  | from config import Config | ||||||
|  | from formation_spawner import FormationSpawner | ||||||
|  | from game import Game | ||||||
|  | from hud import GameHud | ||||||
|  | from leaderboard import Leaderboard | ||||||
|  | from menu import KeybindsMenu, Menu | ||||||
|  | from shooter import Shooter, ShooterAttributes | ||||||
|  | from textures import Textures | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class GameState(Enum): | ||||||
|  |     """Enum of possible game states""" | ||||||
|  | 
 | ||||||
|  |     MAIN_MENU = auto() | ||||||
|  |     GAME = auto() | ||||||
|  |     PAUSED = auto() | ||||||
|  |     END_LEADERBOARD = auto() | ||||||
|  |     LEADERBOARD = auto() | ||||||
|  |     SETTINGS = auto() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class GameSave: | ||||||
|  |     """Static class for saving and loading game""" | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def save_game(game): | ||||||
|  |         """Save game state to a file | ||||||
|  | 
 | ||||||
|  |         :param game: Game to save | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         phase = int.to_bytes(game.formation_spawner.phase, 2, "big") | ||||||
|  |         hp = int.to_bytes(game.player.hp, 1, "big") | ||||||
|  |         score = int.to_bytes(game.score, 8, "big") | ||||||
|  | 
 | ||||||
|  |         with open(Config.SAVE_FILE, "wb") as file: | ||||||
|  |             file.write(phase) | ||||||
|  |             file.write(hp) | ||||||
|  |             file.write(score) | ||||||
|  | 
 | ||||||
|  |         if not game.menu.has_item("Continue"): | ||||||
|  |             game.menu.add_item("Continue", game.restore_game, index=0) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def load_game(game): | ||||||
|  |         """load game state from file | ||||||
|  | 
 | ||||||
|  |         :param game: Game to load | ||||||
|  |         """ | ||||||
|  |         with open(Config.SAVE_FILE, "rb") as file: | ||||||
|  |             game.formation_spawner.phase = int.from_bytes(file.read(2), "big") | ||||||
|  |             game.player.hp = int.from_bytes(file.read(1), "big") | ||||||
|  |             game.score = int.from_bytes(file.read(8), "big") | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def remove_save(game): | ||||||
|  |         """Remove the game save file | ||||||
|  | 
 | ||||||
|  |         :param game: | ||||||
|  |         """ | ||||||
|  |         if path.exists(Config.SAVE_FILE): | ||||||
|  |             remove(Config.SAVE_FILE) | ||||||
|  |             if game.menu.has_item("Continue"): | ||||||
|  |                 game.menu.del_item("Continue") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Player(Shooter): | ||||||
|  |     """Controllable player object""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, game: Game): | ||||||
|  |         """Initialise the player | ||||||
|  | 
 | ||||||
|  |         :param game: The game which this belongs to | ||||||
|  |         :type game: Game | ||||||
|  |         """ | ||||||
|  |         attributes = ShooterAttributes( | ||||||
|  |             cooldown=12, | ||||||
|  |             velocity=-2, | ||||||
|  |             hp=10 | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         super().__init__(game, "ship", attributes) | ||||||
|  |         self.set_pos( | ||||||
|  |             ((self.game.w - self.w) // 2, (self.game.h - self.h))) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update this object""" | ||||||
|  |         super().tick() | ||||||
|  |         if self.game.inputs.k_left: | ||||||
|  |             self.move(-1, 0) | ||||||
|  |         if self.game.inputs.k_right: | ||||||
|  |             self.move(1, 0) | ||||||
|  |         if self.game.inputs.k_up: | ||||||
|  |             self.move(0, -1) | ||||||
|  |         if self.game.inputs.k_down: | ||||||
|  |             self.move(0, 1) | ||||||
|  | 
 | ||||||
|  |         # clamp the player to the screen | ||||||
|  |         if self.x < 0: | ||||||
|  |             self.set_pos((0, self.y)) | ||||||
|  |         if self.y < 0: | ||||||
|  |             self.set_pos((self.x, 0)) | ||||||
|  |         if self.x + self.w > self.game.w: | ||||||
|  |             self.set_pos((self.game.w - self.w, self.y)) | ||||||
|  |         if self.y + self.h > self.game.h: | ||||||
|  |             self.set_pos((self.x, self.game.h - self.h)) | ||||||
|  | 
 | ||||||
|  |         if self.game.inputs.k_action: | ||||||
|  |             self.shoot() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ShooterGame(Game): | ||||||
|  |     """Game with menus and enemies to be shot at """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         """Initialise the game""" | ||||||
|  |         super().__init__() | ||||||
|  | 
 | ||||||
|  |         self.state = GameState.MAIN_MENU | ||||||
|  |         self.death_time = -1 | ||||||
|  |         self.paused_frame = 0 | ||||||
|  | 
 | ||||||
|  |         # load textures | ||||||
|  |         Textures.load_textures(self.texture_factory) | ||||||
|  |         self.effect_player.load_textures() | ||||||
|  |         self.effect_player.create_stars() | ||||||
|  | 
 | ||||||
|  |         self.state = GameState.MAIN_MENU | ||||||
|  | 
 | ||||||
|  |         self.formation_spawner = FormationSpawner(self) | ||||||
|  | 
 | ||||||
|  |         self.player = Player(self) | ||||||
|  | 
 | ||||||
|  |         # make the game hud last to make sure its ontop | ||||||
|  |         self.game_hud = GameHud(self) | ||||||
|  | 
 | ||||||
|  |         # create the leaderboard sprites | ||||||
|  |         self.leaderboard = Leaderboard(self) | ||||||
|  |         self.leaderboard.callback = self.show_menu | ||||||
|  | 
 | ||||||
|  |         # make the settings menu | ||||||
|  |         self.settings_menu = KeybindsMenu(self, "Keybinds") | ||||||
|  |         for name, value in vars(self.inputs.settings).items(): | ||||||
|  |             label = self.settings_menu.get_label(name, value) | ||||||
|  |             self.settings_menu.add_item( | ||||||
|  |                 label, | ||||||
|  |                 self.settings_menu.get_set_keybind(name) | ||||||
|  |             ) | ||||||
|  |         self.settings_menu.add_item("Return", self.show_menu) | ||||||
|  | 
 | ||||||
|  |         # make the main menu | ||||||
|  |         self.menu = Menu(self, "Main Menu") | ||||||
|  |         if path.exists(Config.SAVE_FILE): | ||||||
|  |             self.menu.add_item("Continue", self.restore_game) | ||||||
|  |         self.menu.add_item("New Game", self.start_game) | ||||||
|  |         self.menu.add_item("Leaderboard", self.show_leaderboard) | ||||||
|  |         self.menu.add_item("Settings", self.show_settings) | ||||||
|  |         self.menu.show() | ||||||
|  | 
 | ||||||
|  |         # make the pause menu | ||||||
|  |         self.pause_menu = Menu(self, "Game Paused") | ||||||
|  |         self.pause_menu.add_item("Resume", self.resume_game) | ||||||
|  | 
 | ||||||
|  |         self.pause_menu.add_item("Save", lambda: ( | ||||||
|  |             self.save_game(), | ||||||
|  |             self.effect_player.splash_text("Game saved"), | ||||||
|  |             self.resume_game()) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         self.pause_menu.add_item("Exit", self.show_menu) | ||||||
|  | 
 | ||||||
|  |         # initialise cheats | ||||||
|  |         self.cheat_engine = CheatEngine(self) | ||||||
|  |         self.cheat_engine.add_cheat( | ||||||
|  |             Cheat(self, list("test"), | ||||||
|  |                   (lambda: self.effect_player.splash_text("test ok")) | ||||||
|  |                   )) | ||||||
|  | 
 | ||||||
|  |         self.cheat_engine.add_cheat( | ||||||
|  |             Cheat(self, [ | ||||||
|  |                 "Up", | ||||||
|  |                 "Up", | ||||||
|  |                 "Down", | ||||||
|  |                 "Down", | ||||||
|  |                 "Left", | ||||||
|  |                 "Right", | ||||||
|  |                 "Left", | ||||||
|  |                 "Right", | ||||||
|  |                 "b", | ||||||
|  |                 "a", | ||||||
|  |             ], | ||||||
|  |                 (lambda: [self.formation_spawner.spawn_rectangle() | ||||||
|  |                  for _ in range(20)]) | ||||||
|  |             )) | ||||||
|  | 
 | ||||||
|  |         self.cheat_engine.add_cheat(DevModeCheat(self, [ | ||||||
|  |             "Left", | ||||||
|  |             "Right", | ||||||
|  |             "Left", | ||||||
|  |             "Right", | ||||||
|  |             "Escape", | ||||||
|  |             "d", | ||||||
|  |             "Up", | ||||||
|  |             "t", | ||||||
|  |             "b", | ||||||
|  |             "b", | ||||||
|  |             "a", | ||||||
|  |             "b", | ||||||
|  |             "s" | ||||||
|  |         ])) | ||||||
|  | 
 | ||||||
|  |         self.cheat_engine.add_cheat(InvincibilityCheat(self, list("xyzzy"))) | ||||||
|  | 
 | ||||||
|  |         self.boss_key = BossKey(self, self.pause_game) | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update the game state""" | ||||||
|  |         if self.state != GameState.PAUSED: | ||||||
|  |             super().tick() | ||||||
|  | 
 | ||||||
|  |             if random() > 0.9: | ||||||
|  |                 self.effect_player.create_star() | ||||||
|  | 
 | ||||||
|  |         if self.state == GameState.MAIN_MENU: | ||||||
|  |             self.menu.tick() | ||||||
|  |         elif self.state == GameState.SETTINGS: | ||||||
|  |             self.settings_menu.tick() | ||||||
|  |         elif self.state == GameState.GAME: | ||||||
|  |             self.tick_game() | ||||||
|  |         elif self.state == GameState.PAUSED: | ||||||
|  |             self.alpha = self.paused_frame | ||||||
|  |             self.pause_menu.tick() | ||||||
|  |         elif self.state == GameState.END_LEADERBOARD: | ||||||
|  |             self.leaderboard.tick() | ||||||
|  |         elif self.state == GameState.LEADERBOARD: | ||||||
|  |             self.leaderboard.tick() | ||||||
|  | 
 | ||||||
|  |     def tick_game(self): | ||||||
|  |         """Update the game during game play""" | ||||||
|  |         self.game_hud.tick() | ||||||
|  |         self.formation_spawner.tick() | ||||||
|  |         self.player.tick() | ||||||
|  | 
 | ||||||
|  |         if self.player.destroyed: | ||||||
|  |             if self.death_time == -1: | ||||||
|  |                 self.death_time = self.alpha | ||||||
|  |                 self.effect_player.splash_text("GAME OVER", 100) | ||||||
|  |             elif self.alpha - self.death_time > 100: | ||||||
|  |                 self.show_score() | ||||||
|  | 
 | ||||||
|  |         if self.inputs.k_pause: | ||||||
|  |             self.pause_game() | ||||||
|  | 
 | ||||||
|  |     def pause_game(self): | ||||||
|  |         """Set the game to paused state""" | ||||||
|  |         if self.state == GameState.GAME: | ||||||
|  |             self.state = GameState.PAUSED | ||||||
|  | 
 | ||||||
|  |             self.paused_frame = self.alpha | ||||||
|  |             self.pause_menu.show() | ||||||
|  | 
 | ||||||
|  |     def resume_game(self): | ||||||
|  |         """Resume the game from paused state""" | ||||||
|  |         self.state = GameState.GAME | ||||||
|  | 
 | ||||||
|  |         self.pause_menu.hide() | ||||||
|  | 
 | ||||||
|  |     def start_game(self): | ||||||
|  |         """Start a new game""" | ||||||
|  |         self.state = GameState.GAME | ||||||
|  | 
 | ||||||
|  |         GameSave.remove_save(self) | ||||||
|  | 
 | ||||||
|  |         self.menu.hide() | ||||||
|  |         self.pause_menu.hide() | ||||||
|  |         self.formation_spawner.phase = -1 | ||||||
|  |         self.clear_all() | ||||||
|  | 
 | ||||||
|  |         self.score = 0 | ||||||
|  |         self.player = Player(self) | ||||||
|  | 
 | ||||||
|  |         self.formation_spawner.next_phase() | ||||||
|  | 
 | ||||||
|  |         self.player.show() | ||||||
|  |         self.game_hud.show() | ||||||
|  | 
 | ||||||
|  |         self.death_time = -1 | ||||||
|  | 
 | ||||||
|  |     def show_leaderboard(self): | ||||||
|  |         """Show the game's leaderboard""" | ||||||
|  |         self.state = GameState.LEADERBOARD | ||||||
|  | 
 | ||||||
|  |         self.menu.hide() | ||||||
|  |         self.pause_menu.hide() | ||||||
|  |         self.leaderboard.editing = False | ||||||
|  |         self.leaderboard.populate_entries() | ||||||
|  |         self.leaderboard.start_animation() | ||||||
|  | 
 | ||||||
|  |         self.leaderboard.show() | ||||||
|  | 
 | ||||||
|  |     def show_score(self): | ||||||
|  |         """Allow the user to enter their name into the leaderboard""" | ||||||
|  |         self.state = GameState.END_LEADERBOARD | ||||||
|  | 
 | ||||||
|  |         self.clear_all() | ||||||
|  |         self.game_hud.hide() | ||||||
|  |         self.leaderboard.editing = True | ||||||
|  |         self.leaderboard.populate_entries() | ||||||
|  |         self.leaderboard.start_animation() | ||||||
|  |         self.leaderboard.show() | ||||||
|  |         GameSave.remove_save(self) | ||||||
|  | 
 | ||||||
|  |     def show_menu(self): | ||||||
|  |         """Show the main menu""" | ||||||
|  |         self.state = GameState.MAIN_MENU | ||||||
|  | 
 | ||||||
|  |         self.clear_all() | ||||||
|  |         self.leaderboard.hide() | ||||||
|  |         self.game_hud.hide() | ||||||
|  |         self.player.hide() | ||||||
|  |         self.pause_menu.hide() | ||||||
|  |         self.settings_menu.hide() | ||||||
|  | 
 | ||||||
|  |         self.menu.show() | ||||||
|  | 
 | ||||||
|  |     def clear_all(self): | ||||||
|  |         """Remove all the associated game objects""" | ||||||
|  |         self.formation_spawner.clear_all() | ||||||
|  |         self.player.destroy() | ||||||
|  | 
 | ||||||
|  |     def restore_game(self): | ||||||
|  |         """Restore the game's state from file""" | ||||||
|  |         self.state = GameState.GAME | ||||||
|  | 
 | ||||||
|  |         self.menu.hide() | ||||||
|  |         self.pause_menu.hide() | ||||||
|  |         self.clear_all() | ||||||
|  | 
 | ||||||
|  |         self.player = Player(self) | ||||||
|  |         self.death_time = -1 | ||||||
|  | 
 | ||||||
|  |         GameSave.load_game(self) | ||||||
|  | 
 | ||||||
|  |         self.formation_spawner.start_phase() | ||||||
|  |         self.game_hud.show() | ||||||
|  |         self.player.show() | ||||||
|  | 
 | ||||||
|  |     def save_game(self): | ||||||
|  |         """Save the game's state to a file""" | ||||||
|  |         GameSave.save_game(self) | ||||||
|  | 
 | ||||||
|  |     def show_settings(self): | ||||||
|  |         """Show the keybind setting menu""" | ||||||
|  |         self.state = GameState.SETTINGS | ||||||
|  | 
 | ||||||
|  |         self.menu.hide() | ||||||
|  |         self.settings_menu.show() | ||||||
							
								
								
									
										127
									
								
								sprite.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								sprite.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,127 @@ | ||||||
|  | from tkinter import Canvas, NW, PhotoImage | ||||||
|  | 
 | ||||||
|  | from config import Config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Sprite: | ||||||
|  |     """Sprite.""" | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def remove_destroyed(sprite_list): | ||||||
|  |         """Remove all destroyed sprites from a list | ||||||
|  | 
 | ||||||
|  |         :param sprite_list: | ||||||
|  |         :type sprite_list: list[Sprite] | ||||||
|  |         """ | ||||||
|  |         return list(filter(lambda s: not s.destroyed, sprite_list)) | ||||||
|  | 
 | ||||||
|  |     def __init__(self, canvas: Canvas, image: PhotoImage, position=(0, 0)): | ||||||
|  |         """Initialise the sprite class | ||||||
|  | 
 | ||||||
|  |         :param canvas: The canvas to draw the sprites to | ||||||
|  |         :type canvas: Canvas | ||||||
|  |         :param image: The image to be used for the sprite | ||||||
|  |         :type image: PhotoImage | ||||||
|  |         :param position: The default position to place the sprite | ||||||
|  |         """ | ||||||
|  |         # set positions | ||||||
|  |         self.x, self.y = position | ||||||
|  | 
 | ||||||
|  |         self.canvas = canvas | ||||||
|  |         self.canvas_image = canvas.create_image( | ||||||
|  |             self.x * Config.SCALE, self.y * Config.SCALE, | ||||||
|  |             anchor=NW, image=image, state="hidden") | ||||||
|  | 
 | ||||||
|  |         # get pixel width and heigh ignoring scale | ||||||
|  |         self.w = image.width() // Config.SCALE | ||||||
|  |         self.h = image.height() // Config.SCALE | ||||||
|  | 
 | ||||||
|  |         self.destroyed = False | ||||||
|  |         self.hide() | ||||||
|  | 
 | ||||||
|  |     def update_position(self): | ||||||
|  |         """Move the image to the sprites position""" | ||||||
|  |         self.canvas.coords(self.canvas_image, self.x * | ||||||
|  |                            Config.SCALE, self.y*Config.SCALE) | ||||||
|  | 
 | ||||||
|  |     def set_pos(self, pos): | ||||||
|  |         """Set the player position | ||||||
|  | 
 | ||||||
|  |         :param pos: Position to move to | ||||||
|  |         """ | ||||||
|  |         self.x, self.y = pos | ||||||
|  |         self.update_position() | ||||||
|  | 
 | ||||||
|  |     def get_pos(self): | ||||||
|  |         """Return the current position of the sprite""" | ||||||
|  |         return (self.x, self.y) | ||||||
|  | 
 | ||||||
|  |     def move(self, x, y): | ||||||
|  |         """Move the sprite by x and y | ||||||
|  | 
 | ||||||
|  |         :param x: the number of pixels right to move | ||||||
|  |         :param y: the number of pixels down to move | ||||||
|  |         """ | ||||||
|  |         self.x += x | ||||||
|  |         self.y += y | ||||||
|  |         self.update_position() | ||||||
|  | 
 | ||||||
|  |     def collides(self, other): | ||||||
|  |         """Check if the sprite collides with another sprite | ||||||
|  | 
 | ||||||
|  |         :param other: The other sprite | ||||||
|  |         """ | ||||||
|  |         return self.x < other.x + other.w \ | ||||||
|  |             and self.x + self.w > other.x \ | ||||||
|  |             and self.y < other.y + other.h \ | ||||||
|  |             and self.h + self.y > other.y | ||||||
|  | 
 | ||||||
|  |     def collide_all(self, others): | ||||||
|  |         """Check if the sprite collides with a list of sprites | ||||||
|  | 
 | ||||||
|  |         :param others: Array of other sprites to check if collides with | ||||||
|  |         :returns:      index of the sprite that it collided with first | ||||||
|  |                        or -1 if not colliding | ||||||
|  |         """ | ||||||
|  |         for i, other in enumerate(others): | ||||||
|  |             if self.collides(other): | ||||||
|  |                 return i | ||||||
|  |         return -1 | ||||||
|  | 
 | ||||||
|  |     def tick(self): | ||||||
|  |         """Update the sprite""" | ||||||
|  | 
 | ||||||
|  |     def destroy(self): | ||||||
|  |         """Remove the image from the canvas""" | ||||||
|  |         self.canvas.delete(self.canvas_image) | ||||||
|  |         self.destroyed = True | ||||||
|  | 
 | ||||||
|  |     def send_to_front(self): | ||||||
|  |         """Move the sprite to the foreground""" | ||||||
|  |         self.canvas.tag_raise(self.canvas_image) | ||||||
|  | 
 | ||||||
|  |     def send_to_back(self): | ||||||
|  |         """Move the sprite to the background""" | ||||||
|  |         self.canvas.tag_lower(self.canvas_image) | ||||||
|  | 
 | ||||||
|  |     def set_image(self, image: PhotoImage): | ||||||
|  |         """Change the image used by the sprite | ||||||
|  | 
 | ||||||
|  |         :param image: the image to set the sprite to | ||||||
|  |         :type image: PhotoImage | ||||||
|  |         """ | ||||||
|  |         self.canvas.itemconfig(self.canvas_image, image=image) | ||||||
|  | 
 | ||||||
|  |     def show(self): | ||||||
|  |         """Set the sprite to be shown""" | ||||||
|  |         self.canvas.itemconfig(self.canvas_image, state="normal") | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def hide(self): | ||||||
|  |         """Set the sprite to be hidden""" | ||||||
|  |         self.canvas.itemconfig(self.canvas_image, state="hidden") | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def is_hidden(self): | ||||||
|  |         """Return True if the sprite is hidden""" | ||||||
|  |         return self.canvas.itemcget(self.canvas_image, "state") == "hidden" | ||||||
							
								
								
									
										406
									
								
								textures.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										406
									
								
								textures.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,406 @@ | ||||||
|  | from tkinter import PhotoImage | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # tell pylint to ignore long lines in this file, since they make more sense | ||||||
|  | # to not be linewrapped | ||||||
|  | # | ||||||
|  | #   ignore a particular guideline "when applying the guideline would make the code less readable" | ||||||
|  | # https://peps.python.org/pep-0008 | ||||||
|  | # | ||||||
|  | # pylint: disable=line-too-long | ||||||
|  | class Textures: | ||||||
|  |     """Static class containing game textures""" | ||||||
|  |     STAR = [ | ||||||
|  |             ["#AAAAAA"] | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  |     ENEMY = [ | ||||||
|  |     [ | ||||||
|  |         [None,      "#00E436", "#008751", "#008751", None,      None,      None,     ], | ||||||
|  |         ["#00E436", "#00E436", "#00E436", None,      None,      "#008751", None,     ], | ||||||
|  |         ["#008751", "#00E436", "#00E436", None,      None,      "#00E436", "#008751",], | ||||||
|  |         [None,      "#008751", "#008751", None,      "#00E436", "#00E436", "#008751",], | ||||||
|  |         [None,      None,      None,      "#00E436", "#00E436", "#00E436", "#00E436",], | ||||||
|  |         [None,      None,      "#00E436", "#00E436", "#1D2B53", "#00E436", "#008751",], | ||||||
|  |         [None,      None,      "#008751", "#00E436", "#1D2B53", "#1D2B53", "#00E436",], | ||||||
|  |         [None,      None,      "#008751", "#008751", "#00E436", "#FF004D", "#00E436",], | ||||||
|  |         [None,      "#1D2B53", None,      "#008751", "#008751", "#00E436", "#00E436",], | ||||||
|  |         [None,      "#008751", "#00E436", None,      "#008751", "#1D2B53", "#1D2B53",], | ||||||
|  |         ["#008751", "#00E436", "#00E436", "#00E436", None,      "#1D2B53", "#008751",], | ||||||
|  |         ["#00E436", "#00E436", "#00E436", None,      None,      None,      None,     ], | ||||||
|  |         ["#008751", "#00E436", "#00E436", "#00E436", "#008751", None,      None,     ], | ||||||
|  |         ["#1D2B53", "#008751", "#00E436", "#008751", None,      None,      None,     ], | ||||||
|  |     ], | ||||||
|  |     [ | ||||||
|  |         [None,      None,      "#C2C3C7", "#1D2B53", None,      None,      None,     ], | ||||||
|  |         [None,      None,      "#83769C", "#C2C3C7", "#1D2B53", None,      None,     ], | ||||||
|  |         [None,      None,      None,      "#83769C", "#C2C3C7", "#83769C", "#C2C3C7",], | ||||||
|  |         ["#83769C", "#1D2B53", None,      None,      "#83769C", "#C2C3C7", "#C2C3C7",], | ||||||
|  |         ["#83769C", "#83769C", "#83769C", "#1D2B53", "#C2C3C7", "#83769C", "#C2C3C7",], | ||||||
|  |         ["#1D2B53", "#C2C3C7", "#83769C", "#83769C", "#83769C", "#7E2553", "#1D2B53",], | ||||||
|  |         ["#C2C3C7", "#C2C3C7", "#1D2B53", None,      "#C2C3C7", "#7E2553", "#7E2553",], | ||||||
|  |         ["#C2C3C7", "#C2C3C7", None,      None,      "#83769C", "#C2C3C7", "#C2C3C7",], | ||||||
|  |         ["#C2C3C7", "#C2C3C7", "#C2C3C7", "#83769C", "#1D2B53", None,      None,     ], | ||||||
|  |         ["#83769C", "#C2C3C7", "#C2C3C7", "#83769C", None,      None,      None,     ], | ||||||
|  |         ["#1D2B53", "#C2C3C7", "#83769C", "#1D2B53", None,      None,      None,     ], | ||||||
|  |         [None,      "#C2C3C7", "#83769C", "#83769C", None,      None,      None,     ], | ||||||
|  |         [None,      "#1D2B53", "#83769C", "#1D2B53", None,      None,      None,     ], | ||||||
|  |     ], | ||||||
|  |     [ | ||||||
|  |         [None,      None,      None,      None,      "#7E2553", "#FFA300", "#FFA300",], | ||||||
|  |         [None,      None,      "#7E2553", "#FFA300", "#FFA300", "#FFA300", "#FFEC27",], | ||||||
|  |         [None,      "#7E2553", "#FFA300", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27",], | ||||||
|  |         [None,      "#FFA300", "#FFA300", "#FFEC27", "#FFA300", "#FFEC27", "#FFEC27",], | ||||||
|  |         ["#7E2553", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27", "#FFA300", "#FFA300",], | ||||||
|  |         ["#FFA300", "#FFA300", "#FFEC27", "#FFEC27", "#FFEC27", "#FFEC27", "#FFEC27",], | ||||||
|  |         ["#AB5236", "#AB5236", "#000000", "#000000", "#FFA300", "#FFEC27", "#FFEC27",], | ||||||
|  |         ["#AB5236", "#AB5236", "#1D2B53", "#FF004D", "#000000", "#FFA300", "#FFEC27",], | ||||||
|  |         ["#AB5236", "#FFA300", "#AB5236", "#1D2B53", "#1D2B53", "#AB5236", "#AB5236",], | ||||||
|  |         ["#7E2553", "#AB5236", "#FFA300", "#AB5236", "#AB5236", "#FFA300", "#FFA300",], | ||||||
|  |         ["#7E2553", "#7E2553", "#AB5236", "#AB5236", "#AB5236", "#AB5236", "#AB5236",], | ||||||
|  |         [None,      "#7E2553", "#7E2553", "#7E2553", None,      None,      None,     ], | ||||||
|  |     ] | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     ROCK1 = [ | ||||||
|  |         [None,      None,      "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#1D2B53", None,      None,      None,      None,     ], | ||||||
|  |         [None,      "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None,      None,      None,     ], | ||||||
|  |         ["#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None,      None,     ], | ||||||
|  |         ["#FFA300", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", None,     ], | ||||||
|  |         ["#5F574F", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", None,     ], | ||||||
|  |         ["#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |         [None,      "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |         [None,      "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",], | ||||||
|  |         [None,      "#1D2B53", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",], | ||||||
|  |         [None,      None,      "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",], | ||||||
|  |         [None,      None,      None,      "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None,     ], | ||||||
|  |         [None,      None,      None,      None,      None,      None,      "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None,      None,     ], | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     ROCK2 = [ | ||||||
|  |     [None,      None,      None,      None,      "#FFA300", "#FFA300", "#5F574F", "#FFA300", "#5F574F", None,      None,      None,     ], | ||||||
|  |     [None,      None,      None,      "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#1D2B53", "#1D2B53", None,     ], | ||||||
|  |     [None,      None,      "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",], | ||||||
|  |     [None,      "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |     [None,      "#FFA300", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |     [None,      "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",], | ||||||
|  |     [None,      None,      "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", None,     ], | ||||||
|  |     [None,      None,      None,      "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None,      None,      None,     ], | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     ROCK3 = [ | ||||||
|  |         [None,      None,      None,      None,      None,      None,      None,      None,      None,      "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", None,      None,     ], | ||||||
|  |         [None,      None,      None,      None,      "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None,     ], | ||||||
|  |         [None,      "#FFA300", "#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#1D2B53", "#1D2B53",], | ||||||
|  |         ["#FFA300", "#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#FFA300", "#1D2B53", "#1D2B53",], | ||||||
|  |         ["#FFA300", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53",], | ||||||
|  |         ["#5F574F", "#5F574F", "#1D2B53", "#5F574F", "#5F574F", "#FFA300", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None,     ], | ||||||
|  |         ["#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None,      None,      None,      None,      None,      None,      None,      None,      None,     ], | ||||||
|  |         [None,      "#1D2B53", "#1D2B53", "#1D2B53", "#1D2B53", None,      None,      None,      None,      None,      None,      None,      None,      None,      None,      None,     ], | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     ROCK4 = [ | ||||||
|  |         [None,      None,      "#5F574F", "#FFA300", "#FFA300", "#5F574F", None,     ], | ||||||
|  |         [None,      "#5F574F", "#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |         [None,      "#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",], | ||||||
|  |         ["#FFA300", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53",], | ||||||
|  |         ["#FFA300", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", "#1D2B53", None,     ], | ||||||
|  |         ["#5F574F", "#5F574F", "#1D2B53", "#1D2B53", "#1D2B53", None,      None,     ], | ||||||
|  |         [None,      "#1D2B53", "#1D2B53", "#1D2B53", None,      None,      None,     ], | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     ROCK5 = [ | ||||||
|  |         [None,      "#FFA300", "#5F574F", "#5F574F", "#1D2B53", None,     ], | ||||||
|  |         ["#FFA300", "#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |         ["#5F574F", "#5F574F", "#5F574F", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |         ["#1D2B53", "#5F574F", "#5F574F", "#5F574F", "#1D2B53", None,     ], | ||||||
|  |         [None,      "#1D2B53", "#1D2B53", "#1D2B53", None,      None,     ], | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     SHIP = [ | ||||||
|  |         [None,      None,      None,      None,      None,      None,      None,      None,     ], | ||||||
|  |         [None,      None,      None,      "#83769C", "#83769C", None,      None,      None,     ], | ||||||
|  |         [None,      "#83769C", "#1D2B53", "#FFF1E8", "#29ADFF", "#1D2B53", "#5F574F", None,     ], | ||||||
|  |         ["#83769C", "#83769C", "#7E2553", "#FFF1E8", "#29ADFF", "#1D2B53", "#5F574F", "#5F574F",], | ||||||
|  |         ["#83769C", "#C2C3C7", "#83769C", "#29ADFF", "#29ADFF", "#1D2B53", "#83769C", "#5F574F",], | ||||||
|  |         ["#83769C", "#C2C3C7", "#83769C", "#5F574F", "#1D2B53", "#1D2B53", "#83769C", "#5F574F",], | ||||||
|  |         ["#1D2B53", "#83769C", "#5F574F", "#83769C", "#83769C", "#5F574F", "#5F574F", "#1D2B53",], | ||||||
|  |         [None,      "#1D2B53", "#1D2B53", None,      None,      "#1D2B53", "#1D2B53", None,     ], | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     UFO = [ | ||||||
|  |         [None,      None,      None,      None,      None,      "#1D2B53", "#29ADFF", "#FFF1E8",], | ||||||
|  |         [None,      None,      None,      None,      "#1D2B53", "#29ADFF", "#FFF1E8", "#FFF1E8",], | ||||||
|  |         [None,      None,      None,      "#1D2B53", "#29ADFF", "#29ADFF", "#29ADFF", "#29ADFF",], | ||||||
|  |         [None,      "#83769C", "#83769C", "#C2C3C7", "#FFF1E8", "#FFF1E8", "#C2C3C7", "#C2C3C7",], | ||||||
|  |         ["#83769C", "#83769C", "#C2C3C7", "#FFF1E8", "#FFF1E8", "#C2C3C7", "#C2C3C7", "#C2C3C7",], | ||||||
|  |         [None,      "#7E2553", "#FF004D", "#7E2553", None,      None,      "#7E2553", "#FF004D",], | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     LAZER = [ | ||||||
|  |         ["#8F8F8F"], | ||||||
|  |         ["#F8F8F8"], | ||||||
|  |         ["#F8F8F8"], | ||||||
|  |         ["#F8F8F8"], | ||||||
|  |         ["#F8F8F8"], | ||||||
|  |         ["#8F8F8F"] | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     SMALLENEMY =[ | ||||||
|  |         [ | ||||||
|  |             [None,      "#00E436", "#008751", None,     ], | ||||||
|  |             ["#00E436", "#008751", None,      None,     ], | ||||||
|  |             ["#008751", "#008751", "#00E436", "#00E436",], | ||||||
|  |             [None,      "#00E436", "#1D2B53", "#00E436",], | ||||||
|  |             [None,      "#00E436", "#FF004D", "#00E436",], | ||||||
|  |             ["#008751", "#00E436", "#008751", "#008751",], | ||||||
|  |             ["#00E436", "#1D2B53", None,      None,     ], | ||||||
|  |             ["#008751", "#00E436", "#00E436", None,     ], | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             [None,      "#7E2553", "#FFA300",], | ||||||
|  |             ["#7E2553", "#FFA300", "#FFEC27",], | ||||||
|  |             ["#AB5236", "#FFA300", "#FFA300",], | ||||||
|  |             ["#AB5236", "#1D2B53", "#AB5236",], | ||||||
|  |             ["#7E2553", "#AB5236", "#FFA300",], | ||||||
|  |             [None,      None,      None,     ], | ||||||
|  |             ["#FFA300", "#AB5236", None,     ], | ||||||
|  |             ["#AB5236", "#7E2553", None,     ], | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             ["#FF004D", None,      "#7E2553", "#FF004D",], | ||||||
|  |             ["#7E2553", "#7E2553", "#FF004D", "#FF77A8",], | ||||||
|  |             [None,      "#FF004D", "#FF004D", "#FF004D",], | ||||||
|  |             [None,      "#7E2553", None,      "#7E2553",], | ||||||
|  |             ["#7E2553", "#FF004D", "#1D2B53", "#7E2553",], | ||||||
|  |             ["#FF004D", "#FF004D", None,      "#FF004D",], | ||||||
|  |             ["#FF004D", "#7E2553", None,      None,     ], | ||||||
|  |             ["#7E2553", "#FF004D", "#7E2553", None,     ], | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             [None,      "#1D2B53", "#008751",], | ||||||
|  |             [None,      "#008751", "#00E436",], | ||||||
|  |             ["#1D2B53", "#008751", "#00E436",], | ||||||
|  |             ["#1D2B53", "#7E2553", "#008751",], | ||||||
|  |             [None,      "#1D2B53", "#008751",], | ||||||
|  |             ["#008751", "#1D2B53", None,     ], | ||||||
|  |             ["#008751", None,      None,     ], | ||||||
|  |             ["#1D2B53", "#008751", None,     ], | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             [None,      None,      "#FFA300", "#FFEC27",], | ||||||
|  |             [None,      "#FFA300", "#FFEC27", "#FFEC27",], | ||||||
|  |             ["#AB5236", "#FFA300", "#1D2B53", "#FFA300",], | ||||||
|  |             ["#FFA300", "#FFEC27", "#FFA300", "#FFEC27",], | ||||||
|  |             ["#FFEC27", "#FFA300", None,      "#FFEC27",], | ||||||
|  |             ["#FFA300", None,      None,      None,     ], | ||||||
|  |             ["#AB5236", "#FFA300", None,      "#AB5236",], | ||||||
|  |             [None,      "#AB5236", None,      None,     ], | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             [None,      None,      "#7E2553", "#FF77A8",], | ||||||
|  |             [None,      "#7E2553", "#FF77A8", "#FFCCAA",], | ||||||
|  |             [None,      "#FF77A8", "#FFCCAA", "#FFCCAA",], | ||||||
|  |             [None,      "#FFCCAA", "#1D2B53", "#FF77A8",], | ||||||
|  |             [None,      "#7E2553", "#FF77A8", "#FFCCAA",], | ||||||
|  |             [None,      "#FF77A8", "#1D2B53", "#FFCCAA",], | ||||||
|  |             [None,      "#FF77A8", "#1D2B53", "#FF77A8",], | ||||||
|  |             [None,      "#7E2553", "#FF77A8", "#7E2553",], | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             ["#1D2B53", "#83769C", "#1D2B53", None,     ], | ||||||
|  |             [None,      "#1D2B53", "#C2C3C7", None,     ], | ||||||
|  |             ["#83769C", "#1D2B53", "#83769C", "#C2C3C7",], | ||||||
|  |             ["#C2C3C7", "#83769C", "#C2C3C7", "#C2C3C7",], | ||||||
|  |             ["#C2C3C7", "#1D2B53", "#7E2553", "#83769C",], | ||||||
|  |             ["#C2C3C7", "#1D2B53", "#C2C3C7", "#C2C3C7",], | ||||||
|  |             ["#1D2B53", "#83769C", "#1D2B53", None,     ], | ||||||
|  |             [None,      "#1D2B53", "#83769C", None,     ], | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             ["#1D2B53", None,      None,     ], | ||||||
|  |             ["#29ADFF", None,      None,     ], | ||||||
|  |             ["#29ADFF", "#1D2B53", None,     ], | ||||||
|  |             ["#1D2B53", "#29ADFF", "#1D2B53",], | ||||||
|  |             ["#29ADFF", None,      "#29ADFF",], | ||||||
|  |             ["#29ADFF", "#29ADFF", None,     ], | ||||||
|  |             ["#29ADFF", "#29ADFF", "#1D2B53",], | ||||||
|  |             ["#29ADFF", "#1D2B53", None,     ], | ||||||
|  |         ] | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     EXPLOSION =[ | ||||||
|  |             [ | ||||||
|  |                 ["#FFEC27", "#FFEC27"], | ||||||
|  |                 ["#FFF1E8", "#FFEC27"], | ||||||
|  |                 ["#7E2553", "#7E2553"] | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 ["#7E2553", "#FFEC27"], | ||||||
|  |                 ["#FFEC27", "#FFF1E8"], | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 [ None,      "#7E2553", "#FFEC27", "#FFEC27"], | ||||||
|  |                 [ "#7E2553", "#FFEC27", "#FFEC27", "#FFF1E8"], | ||||||
|  |                 [ "#FFEC27", "#FFEC27", "#FFF1E8", "#FFF1E8"], | ||||||
|  |                 [ "#FFEC27", "#FFF1E8", "#FFF1E8", "#FFF1E8"], | ||||||
|  |                 ] | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def hmirror_texture(texture): | ||||||
|  |         """Horizontally mirror a texture | ||||||
|  | 
 | ||||||
|  |         :param texture: texture to mirror | ||||||
|  |         """ | ||||||
|  |         return [(row + row[::-1]) for row in texture] | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def vmirror_texture(texture): | ||||||
|  |         """Vertically mirror a texture | ||||||
|  | 
 | ||||||
|  |         :param texture: texture to mirror | ||||||
|  |         """ | ||||||
|  |         return texture + texture[::-1] | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def recolor(texture, color): | ||||||
|  |         """recolor a texture | ||||||
|  | 
 | ||||||
|  |         :param texture: texture to recolor | ||||||
|  |         :param color: Color to multiply the texture with | ||||||
|  |         """ | ||||||
|  |         return [[None if col is None else Textures.multiply_colors(col, color) for col in row] for row in texture] | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def multiply_colors(hex1, hex2): | ||||||
|  |         """Multiply two RGB colours | ||||||
|  | 
 | ||||||
|  |         :param hex1: first colour | ||||||
|  |         :param hex2: second colour | ||||||
|  |         """ | ||||||
|  |         color1 = Textures.hex_to_rgb(hex1) | ||||||
|  |         color2 = Textures.hex_to_rgb(hex2) | ||||||
|  |         return Textures.rgb_to_hex([color1[i] * color2[i] for i in range(len(color1))]) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def hex_to_rgb(value): | ||||||
|  |         """Convert a hexadecimal colour value to red green and blue | ||||||
|  | 
 | ||||||
|  |         :param value: hex value | ||||||
|  |         """ | ||||||
|  |         value = value.lstrip('#') | ||||||
|  |         length = len(value) | ||||||
|  |         return tuple(int(value[i:i + length // 3], 16)/256 for i in range(0, length, length // 3)) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def rgb_to_hex(value): | ||||||
|  |         """Convert red green and blue value to a hexadecimal representation | ||||||
|  | 
 | ||||||
|  |         :param value: RGB value | ||||||
|  |         """ | ||||||
|  |         return "#" + "".join(f"{int(v*256):02X}" for v in value) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def white_texture(texture): | ||||||
|  |         """Replace all coloured pixels with white | ||||||
|  | 
 | ||||||
|  |         :param texture: Texture to replace on | ||||||
|  |         """ | ||||||
|  |         return [[None if col is None else "#FFFFFF" for col in row] for row in texture] | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def load_textures(texture_factory): | ||||||
|  |         """Load all textures within this class | ||||||
|  | 
 | ||||||
|  |         :param texture_factory: | ||||||
|  |         """ | ||||||
|  |         texture_factory.load_texture( | ||||||
|  |             "ufo", Textures.hmirror_texture(Textures.UFO)) | ||||||
|  |         texture_factory.load_texture("star", Textures.STAR) | ||||||
|  | 
 | ||||||
|  |         texture_factory.load_texture("ship", Textures.SHIP) | ||||||
|  |         texture_factory.load_texture( | ||||||
|  |             "ship:white", Textures.white_texture(Textures.SHIP)) | ||||||
|  | 
 | ||||||
|  |         texture_factory.load_texture("rock1", Textures.ROCK1) | ||||||
|  |         texture_factory.load_texture("rock2", Textures.ROCK2) | ||||||
|  |         texture_factory.load_texture("rock3", Textures.ROCK3) | ||||||
|  |         texture_factory.load_texture("rock4", Textures.ROCK4) | ||||||
|  |         texture_factory.load_texture("rock5", Textures.ROCK5) | ||||||
|  | 
 | ||||||
|  |         texture_factory.load_texture( | ||||||
|  |             "lazer:white", Textures.recolor(Textures.LAZER, "#ffffff")) | ||||||
|  |         texture_factory.load_texture( | ||||||
|  |             "lazer:red", Textures.recolor(Textures.LAZER, "#f2aaaa")) | ||||||
|  |         texture_factory.load_texture( | ||||||
|  |             "lazer:yellow", Textures.recolor(Textures.LAZER, "#f2ffaa")) | ||||||
|  | 
 | ||||||
|  |         for i, enemy in enumerate(Textures.SMALLENEMY): | ||||||
|  |             name = f"smallenemy{i}" | ||||||
|  |             texture = Textures.hmirror_texture(enemy) | ||||||
|  |             texture_factory.load_texture(name, texture) | ||||||
|  |             texture_factory.load_texture( | ||||||
|  |                 f"{name}:white", Textures.white_texture(texture)) | ||||||
|  |             evil_texture = Textures.recolor(texture, "#FF5555") | ||||||
|  |             texture_factory.load_texture(f"{name}_evil", evil_texture) | ||||||
|  |             texture_factory.load_texture( | ||||||
|  |                 f"{name}_evil:white", Textures.white_texture(evil_texture)) | ||||||
|  | 
 | ||||||
|  |         for i, enemy in enumerate(Textures.ENEMY): | ||||||
|  |             name = f"enemy{i}" | ||||||
|  |             texture_factory.load_texture(name, Textures.hmirror_texture(enemy)) | ||||||
|  |             texture_factory.load_texture( | ||||||
|  |                 f"{name}:white", Textures.white_texture(Textures.hmirror_texture(enemy))) | ||||||
|  | 
 | ||||||
|  |         texture_factory.load_texture("explosion3", Textures.EXPLOSION[0]) | ||||||
|  |         texture_factory.load_texture("explosion2", Textures.hmirror_texture( | ||||||
|  |             Textures.vmirror_texture(Textures.EXPLOSION[1]))) | ||||||
|  |         texture_factory.load_texture("explosion1", Textures.hmirror_texture( | ||||||
|  |             Textures.vmirror_texture(Textures.EXPLOSION[2]))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TextureFactory: | ||||||
|  |     """Object that deals with loading and scaling textures""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, scale) -> None: | ||||||
|  |         """Initialise the texture factory | ||||||
|  | 
 | ||||||
|  |         :param scale: the amount of pixels to upscale by | ||||||
|  |         :rtype: None | ||||||
|  |         """ | ||||||
|  |         self.textures = {} | ||||||
|  |         self.scale = scale | ||||||
|  | 
 | ||||||
|  |     def load_texture(self, namespace, texture_matrix): | ||||||
|  |         """Load and upscale a texture | ||||||
|  | 
 | ||||||
|  |         :param namespace: namespace to save this texture to | ||||||
|  |         :param texture_matrix: A matrix of hex colours that represents the texture | ||||||
|  |         """ | ||||||
|  |         if namespace not in self.textures: | ||||||
|  |             height = len(texture_matrix) * self.scale | ||||||
|  |             width = len(texture_matrix[0]) * self.scale | ||||||
|  |             photo_image = PhotoImage(width=width, height=height) | ||||||
|  | 
 | ||||||
|  |             for matrix_y, row in enumerate(texture_matrix): | ||||||
|  |                 for matrix_x, color in enumerate(row): | ||||||
|  |                     if color is not None: | ||||||
|  |                         pixel_string = ( | ||||||
|  |                             "{" + f"{color} "*self.scale + "} ") * self.scale | ||||||
|  |                         photo_image.put( | ||||||
|  |                             pixel_string, (matrix_x*self.scale, matrix_y*self.scale)) | ||||||
|  | 
 | ||||||
|  |             self.textures[namespace] = photo_image | ||||||
|  |             return photo_image | ||||||
|  |         return self.get_image(namespace) | ||||||
|  | 
 | ||||||
|  |     def get_image(self, namespace): | ||||||
|  |         """Get a loaded image | ||||||
|  | 
 | ||||||
|  |         :param namespace: to load the image from | ||||||
|  |         """ | ||||||
|  |         if namespace not in self.textures: | ||||||
|  |             raise Exception( | ||||||
|  |                 f"Provided namespace \"{namespace}\" has not been loaded!") | ||||||
|  |         return self.textures[namespace] | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue