diff --git a/classes/Audio_controller.py b/classes/Audio_controller.py deleted file mode 100644 index 40c00ff..0000000 --- a/classes/Audio_controller.py +++ /dev/null @@ -1,28 +0,0 @@ -import pygame -from classes.Controller import Controller -from classes.mothership.Mothership import Mothership - -pygame.mixer.init() - - -class AudioController(Controller): - def __init__(self, config): - super().__init__(config) - audio_config = config.get("audio") - self.mothership_sound = pygame.mixer.Sound(audio_config["mothership"]) - self.mothership_bonus_sound = pygame.mixer.Sound( - audio_config["mothership_bonus"] - ) - - def on_mothership_spawned(self, data): - self.mothership_sound.play(-1) - - def on_mothership_exit(self, data): - self.mothership_sound.fadeout(1000) - - def on_mothership_bonus(self, data): - self.mothership_sound.stop() - self.mothership_bonus_sound.play() - - def play_player_shot_sound(self): - pass diff --git a/classes/Baseline_controller.py b/classes/Baseline_controller.py deleted file mode 100644 index db3c211..0000000 --- a/classes/Baseline_controller.py +++ /dev/null @@ -1,53 +0,0 @@ -import pygame, os - - -class BaselineController: - def __init__(self): - self.baselineSprite = pygame.sprite.Sprite() - - self.baselineImage = pygame.image.load( - os.path.join("sprites", "invader_bomb", "bomb_exploding_base.png") - ) - - # create the surface for the sprite - self.baselineSprite.image = pygame.Surface((224, 1), pygame.SRCALPHA) - # define the green color as (R, G, B) tuple - self.baselineSprite.image.fill((0, 255, 0)) - - # Set the rect of the sprite - self.baselineSprite.rect = self.baselineSprite.image.get_rect() - self.baselineSprite.rect.x = 0 - self.baselineSprite.rect.y = 240 - # self.collision_check = pygame.sprite.spritecollide - self.get_bombs_callback = None - - def draw(self, surface): - # draw the baselineSprite onto the specified surface - surface.blit(self.baselineSprite.image, self.baselineSprite.rect.topleft) - - def get_base_row(self, bomb_collision): - bomb_collision.rect.height - 1 - # masked_canvas.blit(bomb_collision.image, (bomb_collision.rect.x, bottom_row_y), special_flags=pygame.BLEND_RGBA_MULT) - - def update(self, events, dt): - if self.get_bombs_callback: - bomb_sprites = self.get_bombs_callback() - if len(bomb_sprites) > 0: - collisions = pygame.sprite.spritecollide( - self.baselineSprite, bomb_sprites, False - ) - if collisions: - for bomb_collision in collisions: - bomb_collision.rect.y = 233 - bomb_collision.active = False - masked_canvas = self.baselineSprite.image.copy() - masked_canvas.blit( - self.baselineImage, - # where to draw on the canvas. here its the x position of the bomb and 0 on the y - (bomb_collision.rect.x, 0), - special_flags=pygame.BLEND_RGBA_MULT, - ) - self.baselineSprite.image = masked_canvas - # bomb_collision.kill() - - return self diff --git a/classes/Config.py b/classes/Config.py index 42e5258..4c7c7ee 100644 --- a/classes/Config.py +++ b/classes/Config.py @@ -1,13 +1,10 @@ import os -base_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - class Config: config_values = { "original_screen_size": (224, 256), "larger_screen_size": (224 * 4, 256 * 4), - "bg_image_path": os.path.join("images", "invaders_moon_bg.png"), "max_fps": 60, "top_left": (0, 0), "ui": { @@ -20,7 +17,6 @@ class Config: }, "shields": { "positions": [(29, 191), (75, 191), (127, 191), (173, 191)], - "image": os.path.join("sprites", "player", "player-shield.png"), }, "invaders": { "spawn_rows": [120, 144, 160, 168, 168, 168, 176, 176, 176], @@ -35,53 +31,12 @@ class Config: "screen_right_limit": 200, "horizontal_move": 2, "vertical_move": 8, - "images": { - "small": [ - os.path.join("sprites", "invader", "invader-small-frame1.png"), - os.path.join("sprites", "invader", "invader-small-frame2.png"), - ], - "mid": [ - os.path.join("sprites", "invader", "invader-mid-frame1.png"), - os.path.join("sprites", "invader", "invader-mid-frame2.png"), - ], - "large": [ - os.path.join("sprites", "invader", "invader-large-frame1.png"), - os.path.join("sprites", "invader", "invader-large-frame2.png"), - ], - }, }, "bombs": { - "exploding_image": os.path.join( - "sprites", "invader_bomb", "bomb_exploding.png" - ), - "images": { - "plunger": [ - os.path.join("sprites", "invader_bomb", "plunger-frame1.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame2.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame3.png"), - os.path.join("sprites", "invader_bomb", "plunger-frame4.png"), - ], - "squiggly": [ - os.path.join("sprites", "invader_bomb", "squiggly-frame1.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame2.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame3.png"), - os.path.join("sprites", "invader_bomb", "squiggly-frame4.png"), - ], - "rolling": [ - os.path.join("sprites", "invader_bomb", "rolling-frame1.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame2.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame3.png"), - os.path.join("sprites", "invader_bomb", "rolling-frame4.png"), - ], - }, "max_bombs": 1, "grace_period": 60, }, "mothership": { - "image_frame": os.path.join("sprites", "mothership", "mothership.png"), - "explode_frame": os.path.join( - "sprites", "mothership", "mothership-exploding.png" - ), # "cycles_with_explosion_frame": 60, # "cycles_with_bonus_text": 60, "cycles_until_spawn": 200, @@ -107,59 +62,13 @@ class Config: 300, ], }, - "font_spritesheet_offsets": { - "A": [1, 1], - "B": [11, 1], - "C": [21, 1], - "D": [31, 1], - "E": [41, 1], - "F": [51, 1], - "G": [61, 1], - "H": [71, 1], - "I": [1, 11], - "J": [11, 11], - "K": [21, 11], - "L": [31, 11], - "M": [41, 11], - "N": [51, 11], - "O": [61, 11], - "P": [71, 11], - "Q": [1, 21], - "R": [11, 21], - "S": [21, 21], - "T": [31, 21], - "U": [41, 21], - "W": [51, 21], - "X": [61, 21], - "Y": [1, 31], - "Z": [11, 31], - "0": [21, 31], - "1": [31, 31], - "2": [41, 31], - "3": [51, 31], - "4": [61, 31], - "5": [71, 31], - "6": [1, 41], - "7": [11, 41], - "8": [21, 41], - "9": [31, 41], - "<": [41, 41], - ">": [51, 41], - "=": [61, 41], - "*": [71, 41], - "?": [1, 51], - "-": [11, 51], - }, "audio": { "mothership": "sounds/mothership.wav", "mothership_bonus": "sounds/mothership_bonus.wav", + "player_explodes": "sounds/player_destroyed.wav", }, } @staticmethod def get(key): return Config.config_values.get(key) - - @staticmethod - def get_file_path(key): - return os.path.join(base_directory, key) diff --git a/classes/Controller.py b/classes/Controller.py deleted file mode 100644 index f14b690..0000000 --- a/classes/Controller.py +++ /dev/null @@ -1,7 +0,0 @@ -from classes.Event_manager import EventManager - - -class Controller: - def __init__(self, config): - self.event_manager = EventManager.get_instance() - self.config = config diff --git a/classes/Debug.py b/classes/Debug.py deleted file mode 100644 index 0355b8b..0000000 --- a/classes/Debug.py +++ /dev/null @@ -1,26 +0,0 @@ -import pygame, os - -font = pygame.font.Font( - os.path.join(os.path.dirname(__file__), "space_invaders.ttf"), 8 -) - - -class Debug: - def __init__(self): - self.debug_requests = [] - - def clear_requests(self): - self.debug_requests = [] - - def add_request(self, x, y, text): - self.debug_requests.append((x, y, text)) - - def get_requests(self): - return self.debug_requests - - def render_requests(self, surface): - for request in self.debug_requests: - x, y, text = request - debug_surf = font.render(text, False, "White") - debug_rect = debug_surf.get_rect(topleft=(x, y)) - surface.blit(debug_surf, debug_rect) diff --git a/classes/Display.py b/classes/Display.py new file mode 100644 index 0000000..49c1033 --- /dev/null +++ b/classes/Display.py @@ -0,0 +1,31 @@ +import pygame +from classes.config.Game_config import GameConfig + + +class Display: + def __init__(self): + config = GameConfig() + self.top_left = config.get("top_left") + self.original_screen_size = config.get("original_screen_size") + self.larger_screen_size = config.get("larger_screen_size") + + _bg_image = pygame.image.load(config.get_file_path(config.get("bg_image_path"))) + self.scaled_image = pygame.transform.scale(_bg_image, self.larger_screen_size) + self.window_surface = pygame.display.set_mode(self.larger_screen_size) + + def update(self, surface_array): + clean_game_surface = pygame.Surface(self.original_screen_size, pygame.SRCALPHA) + for drawable_surface in surface_array: + # print(drawable_surface) + if isinstance(drawable_surface, pygame.sprite.Group): + # if isinstance(drawable_surface, pygame.Surface): + drawable_surface.draw(clean_game_surface) + + # background image is drawn first + self.window_surface.blit(self.scaled_image, self.top_left) + + # scale the playing surface up to target size on main window + self.window_surface.blit( + pygame.transform.scale(clean_game_surface, self.larger_screen_size), + self.top_left, + ) diff --git a/classes/Game_controller.py b/classes/Game_controller.py deleted file mode 100644 index 9ee22dd..0000000 --- a/classes/Game_controller.py +++ /dev/null @@ -1,221 +0,0 @@ -import pygame -import sys - -from classes.Controller import Controller -from classes.invader.Invader_controller import InvaderController -from classes.bomb.Bomb_controller import BombController -from classes.player.Player import Player -from classes.player.Player_controller import PlayerController -from classes.player.Player_missile_controller import PlayerMissileController -from classes.shield.Shield_controller import ShieldController -from classes.player.Player_missile import PlayerMissile -from classes.Baseline_controller import BaselineController -from classes.Input_controller import InputController -from classes.Scoreboard_controller import ScoreboardController -from classes.UI_controller import UIController -from classes.mothership.Mothership_controller import MothershipController -from classes.Audio_controller import AudioController - - -class GameController(Controller): - def __init__(self, config): - super().__init__(config) - self.bombs_enabled = False - self.play_delay_count = 120 - self.player_on_screen = False - - self.player_missile = None - self.invader_swarm_complete = False - self.top_left = config.get("top_left") - self.original_screen_size = config.get("original_screen_size") - self.larger_screen_size = config.get("larger_screen_size") - - _bg_image = pygame.image.load(config.get_file_path(config.get("bg_image_path"))) - - self.scaled_image = pygame.transform.scale(_bg_image, self.larger_screen_size) - # add , pygame.FULLSCREEN to run without border - self.window_surface = pygame.display.set_mode(self.larger_screen_size) - self.max_fps = config.get("max_fps") - - self.setup_controllers(config) - self.setup_controller_callbacks() - self.setup_game_events() - - def setup_controllers(self, config): - self.controllers = { - "invader": InvaderController(config), - "mothership": MothershipController(config), - "player": PlayerController(config), - "shield": ShieldController(config), - "missile": PlayerMissileController(), - "bomb": BombController(config), - "baseline": BaselineController(), - "input": InputController(config), - "score": ScoreboardController(config), - "ui": UIController(config), - "audio": AudioController(config), - } - - def setup_controller_callbacks(self): - self.controllers["baseline"].get_bombs_callback = self.controllers[ - "bomb" - ].get_bombs - - self.controllers["bomb"].get_invaders_callback = self.controllers[ - "invader" - ].get_invaders - - self.controllers["bomb"].get_player_callback = self.controllers[ - "player" - ].get_player - - self.controllers["shield"].get_bombs_callback = self.controllers[ - "bomb" - ].get_bombs - - self.controllers["shield"].get_missile_callback = self.controllers[ - "missile" - ].get_player_missile - - self.controllers["shield"].get_invaders_callback = self.controllers[ - "invader" - ].get_invaders - - self.controllers["missile"].get_player_callback = self.controllers[ - "player" - ].get_player - - self.controllers["missile"].mothership_is_exploding = self.controllers[ - "mothership" - ].mothership_is_exploding - - self.controllers["missile"].get_invaders_callback = self.controllers[ - "invader" - ].get_invaders - - self.controllers["ui"].get_score_callback = self.controllers["score"].get_score - - self.controllers["mothership"].get_score_text_callback = self.controllers[ - "ui" - ].create_text_surface - - self.controllers["mothership"].get_invader_count_callback = self.controllers[ - "invader" - ].get_invader_count - - self.controllers["mothership"].get_lowest_invader_y_callback = self.controllers[ - "invader" - ].get_lowest_invader_y - - self.controllers["mothership"].get_missile_callback = self.controllers[ - "missile" - ].get_player_missile - - def setup_game_events(self): - self.event_manager.add_listener("swarm_complete", self.on_swarm_complete) - - self.event_manager.add_listener( - "mothership_spawned", self.controllers["audio"].on_mothership_spawned - ) - - self.event_manager.add_listener( - "mothership_hit", self.controllers["audio"].on_mothership_bonus - ) - - self.event_manager.add_listener( - "mothership_hit", self.controllers["score"].on_points_awarded - ) - - self.event_manager.add_listener( - "mothership_exit", self.controllers["audio"].on_mothership_exit - ) - - self.event_manager.add_listener( - "points_awarded", self.controllers["score"].on_points_awarded - ) - - self.event_manager.add_listener( - "invader_hit", self.controllers["invader"].on_invader_hit - ) - self.event_manager.add_listener( - "invader_removed", self.controllers["missile"].on_missile_ready - ) - - self.event_manager.add_listener( - "play_delay_complete", self.controllers["player"].on_play_delay_complete - ) - - self.event_manager.add_listener( - "play_delay_complete", self.controllers["missile"].on_missile_ready - ) - - self.event_manager.add_listener( - "play_delay_complete", self.controllers["bomb"].on_play_delay_complete - ) - - self.event_manager.add_listener( - "left_button_pressed", self.controllers["player"].on_move_left - ) - self.event_manager.add_listener( - "left_button_released", self.controllers["player"].on_move_left_exit - ) - self.event_manager.add_listener( - "right_button_pressed", self.controllers["player"].on_move_right - ) - self.event_manager.add_listener( - "right_button_released", self.controllers["player"].on_move_right_exit - ) - - self.event_manager.add_listener( - "fire_button_pressed", self.controllers["missile"].on_fire_pressed - ) - - self.event_manager.add_listener( - "fire_button_pressed", self.controllers["mothership"].on_update_shot_counter - ) - - self.event_manager.add_listener( - "escape_button_pressed", self.on_escape_button_pressed - ) - - def on_escape_button_pressed(self): - pygame.quit() - sys.exit() - - def on_swarm_complete(self, data): - self.invader_swarm_complete = True - - def launch_player_missile(self, player_rect): - self.player_missile = PlayerMissile(player_rect) - - def player_missile_remove(self): - self.player_missile = None - - def update(self, events, dt): - clock = pygame.time.Clock() - - if self.play_delay_count > 0: - self.play_delay_count -= 1 - if self.play_delay_count <= 0: - self.event_manager.notify("play_delay_complete") - - # create a new game surface each frame - game_surface = pygame.Surface(self.original_screen_size, pygame.SRCALPHA) - - for controller in self.controllers.values(): - if hasattr(controller, "update"): - canvas_item = controller.update(events, dt) - # print(canvas_item) - if hasattr(canvas_item, "draw"): - canvas_item.draw(game_surface) - - # render the playing surface onto the main window - self.window_surface.blit(self.scaled_image, self.top_left) - # scale the playing surface up to target size on main window - self.window_surface.blit( - pygame.transform.scale(game_surface, self.larger_screen_size), - self.top_left, - ) - - pygame.display.flip() - clock.tick(self.max_fps) diff --git a/classes/Game_ontroller.py b/classes/Game_ontroller.py new file mode 100644 index 0000000..af6aaec --- /dev/null +++ b/classes/Game_ontroller.py @@ -0,0 +1,180 @@ +import importlib +import inspect +import os +import pygame + +from lib.Controller import Controller +from classes.config.Game_config import GameConfig + +## +## need a state manager system to co-ordinate the screens +## state manager can poll the wipe position before changing state +## state manager can be built into base controller? +## + + +class GameController(Controller): + STARTING_LIVES = 1 + + def __init__(self): + super().__init__() + config = GameConfig() + + # screen wipe settings + self.wipe_x = 0 # Initial position of the wipe effect + self.wipe_speed = 32 # Adjust this to control the speed of the wipe + self.is_wiping = False + + self.lives = self.STARTING_LIVES + self.has_extra_life = True + self.play_delay_count = 120 + self.top_left = config.get("top_left") + self.original_screen_size = config.get("original_screen_size") + self.larger_screen_size = config.get("larger_screen_size") + + _bg_image = pygame.image.load(config.get_file_path(config.get("bg_image_path"))) + + self.scaled_image = pygame.transform.scale(_bg_image, self.larger_screen_size) + # add , pygame.FULLSCREEN to run without border + self.window_surface = pygame.display.set_mode(self.larger_screen_size) + + self.event_manager.add_listener( + "escape_button_pressed", self.on_escape_button_pressed + ) + self.event_manager.add_listener( + "player_explosion_complete", self.on_player_explosion_complete + ) + + self.event_manager.add_listener( + "extra_life_awarded", self.on_extra_life_awarded + ) + + self.event_manager.add_listener("game_over_animation_ended", self.on_begin_wipe) + + self.register_callback("get_lives_count", lambda: self.lives) + self.register_callback("get_extra_life", lambda: self.has_extra_life) + + # self.event_manager.add_listener("game_over_animation_ended", self.on_restart) + + def on_begin_wipe(self, data): + print("starting wipe...") + self.is_wiping = True + + def on_restart(self, data): + for controller in self.controllers: + if hasattr(controller, "game_restart") and callable( + controller.game_restart + ): + controller.game_restart() + + def debug_controllers(self): + print("Ordered Controllers:") + for controller in self.controllers: + print( + f"{controller.__class__.__name__} - Rendering Order: {controller.rendering_order}" + ) + + def load_controllers(self): + # Initialize an empty list to store the imported controllers + self.controllers = [] + + # Construct the full directory path for controllers + controllers_directory = os.path.join("classes", "controllers") + + # Loop through files in the controllers directory + for filename in os.listdir(controllers_directory): + if filename.endswith("_controller.py") and filename != "game_controller.py": + # Extract the module name without the extension + module_name = filename[:-3] + + # Construct the full module path + module_path = f"classes.controllers.{module_name}" + + # Import the module dynamically + module = importlib.import_module(module_path) + + # Check if the module defines a controller class and add it to the list + for name, obj in inspect.getmembers(module): + if ( + inspect.isclass(obj) + and issubclass(obj, Controller) + and obj != Controller + ): + # Create an instance of the controller + controller_instance = obj() + if not hasattr(controller_instance, "rendering_order"): + controller_instance.rendering_order = 0 + + self.controllers.append(controller_instance) + + self.controllers.sort(key=lambda controller: controller.rendering_order) + + # when all controllers have loaded, call the game_ready function + # which is when it is safe to refer between all controllers + for controller_instance in self.controllers: + if hasattr(controller_instance, "game_ready") and callable( + controller_instance.game_ready + ): + controller_instance.game_ready() + + def on_extra_life_awarded(self, data): + self.has_extra_life = False + self.lives += 1 + + def on_escape_button_pressed(self, data): + pygame.quit() + + def on_player_explosion_complete(self, data): + self.lives -= 1 + if self.lives > 0: + self.play_delay_count = 120 + else: + self.event_manager.notify("game_ended") + # manage game over here + return + + def update(self, events, dt): + if self.play_delay_count > 0: + self.play_delay_count -= 1 + if self.play_delay_count <= 0: + self.event_manager.notify("play_delay_complete") + + # create a new game surface each frame + game_surface = pygame.Surface(self.original_screen_size, pygame.SRCALPHA) + # game_surface.fill((0, 0, 0)) + + ## have an intermediate surface that all controllers get blitted to + ## have a background surface which gets the background image + ## have a foreground surface which gets the wipe blitted + ## then blit all of them onto main window + ## could just have the wipe render below the score board + + for controller_instance in self.controllers: + # Call the update method if it exists on the controller + if hasattr(controller_instance, "update"): + canvas_item = controller_instance.update(events, dt) + # Check if the controller returns an object with a "draw" method + if canvas_item and hasattr(canvas_item, "draw"): + canvas_item.draw(game_surface) + + # render the playing surface onto the main window + # if not self.is_wiping: + self.window_surface.blit(self.scaled_image, self.top_left) + + # scale the playing surface up to target size on main window + self.window_surface.blit( + pygame.transform.scale(game_surface, self.larger_screen_size), + self.top_left, + ) + + if self.is_wiping: + if self.wipe_x <= 224 * 4: + self.window_surface.blit( + self.scaled_image, (0, 160), (0, 160, self.wipe_x, 256 * 4) + ) + self.wipe_x += self.wipe_speed + else: + # possibly have a callback to poll the status instead of event callbacks + self.event_manager.notify("wipe_animation_complete") + self.is_wiping = False + self.wipe_x = 0 diff --git a/classes/Modified_sprite.py b/classes/Modified_sprite.py deleted file mode 100644 index 73dfc77..0000000 --- a/classes/Modified_sprite.py +++ /dev/null @@ -1,30 +0,0 @@ -import pygame.sprite - -# global_position_threshold = 200 - - -class ModifiedSprite(pygame.sprite.Sprite): - def __init__(self, image, x, y): - super().__init__() - self.original_image = image - self.image = self.original_image.copy() # Create a copy to modify - self.rect = self.image.get_rect() - self.rect.topleft = (x, y) - - self.modify_pixel_colors() # Modify pixel colors initially - - def modify_pixel_colors(self): - pixel_array = pygame.PixelArray(self.image) - for y in range(self.rect.height): - for x in range(self.rect.width): - global_x = self.rect.x + x - global_y = self.rect.y + y - - if global_y > 150: - pixel_array[x, y] = (0, 255, 0, pixel_array[x, y][3]) - del pixel_array - - def update(self): - # Handle updates here (e.g., changing position) - # Modify pixel colors after each update - self.modify_pixel_colors() diff --git a/classes/Scoreboard_controller.py b/classes/Scoreboard_controller.py deleted file mode 100644 index 016b218..0000000 --- a/classes/Scoreboard_controller.py +++ /dev/null @@ -1,14 +0,0 @@ -from classes.Controller import Controller - - -class ScoreboardController(Controller): - def __init__(self, config): - super().__init__(config) - self.score = 0 - self.update_ui_callback = lambda: None - - def on_points_awarded(self, points): - self.score += points - - def get_score(self): - return str(self.score).zfill(5) diff --git a/classes/State_machine.py b/classes/State_machine.py new file mode 100644 index 0000000..3f5fcc0 --- /dev/null +++ b/classes/State_machine.py @@ -0,0 +1,150 @@ +from classes.Display import Display + + +# idea: have system class that holds event management and controller management and display +# system has methods to delegate to above system +# then get events and controllers out of state machine +class StateMachine: + def __init__(self, states, starting_state): + self.display = Display() + self.states = states + self.state = states[starting_state] + self.enter_state() + + def get_state_name(self): + return self.state.name + + def change_to(self, new_state): + self.state = self.states[new_state] + self.enter_state() + + def update(self, events): + self.state.update(events) + returned_surfaces = self.state.get_surfaces() + self.display.update(returned_surfaces) + + def enter_state(self): + self.state.enter(self) + + +# extends Node + +# class_name StateMachine + +# signal state_bootup_entered +# signal state_crashed_entered +# signal state_demo_crashed_entered +# signal state_demo_over_entered +# signal state_demo_playing_entered +# signal state_game_over_entered +# signal state_game_over_high_scores_entered +# signal state_intro_entered +# signal state_key_controls_entered +# signal state_high_scores_entered +# signal state_player_starts +# signal state_score_table_entered +# signal state_mission_complete_entered +# signal state_playing_entered +# signal state_settings_entered +# signal state_achievements_entered + + +# const DEBUG = true + +# var state: Object +# var mission_complete:bool = false +# var base_destroyed:bool = false +# var demo_mode: bool = false +# var crashed: bool = false +# var lives_depleted: bool = false + + +# func is_demo_mode() -> bool: +# return demo_mode + + +# func set_demo_mode(value: bool) -> void: +# demo_mode = value + + +# func set_player_crashed() -> void: +# crashed = true + +# func clear_player_crashed() -> void: +# crashed = false + + +# func set_mission_completed() -> void: +# # set the flag in case ship is destroyed during base explosion +# # we want to allow for respawn and immediate exit to congrats screen +# mission_complete = true + + +# func set_base_destroyed(_points) -> void: # signal connects with points +# base_destroyed = true + + +# func set_lives_depleted(): +# lives_depleted = true + + +# func clear_lives_depleted(): +# lives_depleted = false + + +# func get_state_name() -> String: +# return state.name + + +# func _ready() -> void: +# # important - need bootup node first to allow delay +# # otherwise signals do not completely load in game.gd +# state = get_node("State_bootup") +# _enter_state() + + +# func change_to(new_state) -> void: +# state = get_node(new_state) +# _enter_state() + + +# func _input(event) -> void: +# if state.has_method("input"): +# state.input(event) + + +# func _unhandled_key_input(event) -> void: +# if state.has_method("unhandled_key_input"): +# state.unhandled_key_input(event) + + +# func _process(delta) -> void: +# if state.has_method("process"): +# state.process(delta) + + +# func _enter_state() -> void: +# if DEBUG: +# print("Entering state: ", state.name) + +# match state.name: +# "State_bootup": emit_signal("state_bootup_entered") +# "State_intro": emit_signal("state_intro_entered") +# "State_settings": emit_signal("state_settings_entered") +# "State_key_controls": emit_signal("state_key_controls_entered") +# "State_achievements": emit_signal("state_achievements_entered") +# "State_high_scores": emit_signal("state_high_scores_entered") +# "State_score_table": emit_signal("state_score_table_entered") +# "State_player_starts": emit_signal("state_player_starts") +# "State_playing": emit_signal("state_playing_entered") +# "State_demo_playing": emit_signal("state_demo_playing_entered") +# "State_demo_crashed": emit_signal("state_demo_crashed_entered") +# "State_demo_over": emit_signal("state_demo_over_entered") +# "State_crashed": emit_signal("state_crashed_entered") +# "State_game_over": emit_signal("state_game_over_entered") +# "State_game_over_high_scores": emit_signal("state_game_over_high_scores_entered") +# "State_mission_complete": emit_signal("state_mission_complete_entered") + +# # Give the new state a reference to this state machine script +# state.fsm = self +# state.enter() diff --git a/classes/System.py b/classes/System.py new file mode 100644 index 0000000..e9064ea --- /dev/null +++ b/classes/System.py @@ -0,0 +1,122 @@ +import importlib +import inspect +import os +from classes.Display import Display +from lib.Event_manager import EventManager +from lib.Controller import Controller + +## dont forget about callback in controllers +## perhaps pass an notifier callback and register callback as params into controller instancing line 70 +## maybe have a callback class and pass a method or object + + +class System: + _instance = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = System() + return cls._instance + + def __init__(self): + self.display = Display() + self.event_manager = EventManager.get_instance() + self.load_controllers() + + # Class-level dictionary to store callbacks + callbacks = {} + + # Class-level dictionary to store callback names (labels) + callback_names = {} + + @classmethod + def register_callback(cls, key, callback, name=None): + cls.callbacks[key] = callback + + # Store the callback name if provided + if name: + cls.callback_names[key] = name + + @classmethod + def get_callback(cls, key): + return cls.callbacks.get(key, None) + + @classmethod + def callback(cls, key): + callback_function = cls.callbacks.get(key) + if callback_function is not None: + return callback_function() + else: + # Optionally handle the case where the key is not found + # print(f"No callback found for key: {key}") + return None + + @classmethod + def debug_callbacks(cls): + print("Callbacks:") + for key, callback in cls.callbacks.items(): + # Get the callback name from the dictionary, or use the key as a fallback + name = cls.callback_names.get(key, key) + print( + f"{name}: {callback.__name__ if hasattr(callback, '__name__') else callback}" + ) + + def add_listener(self, event_type, listener): + self.event_manager.add_listener(event_type, listener) + + def remove_listener(self, event_type, listener): + self.event_manager.remove_listener(event_type, listener) + + def get_controller(self, target_controller): + _controller = next( + ( + controller + for controller in self.controllers + if controller.__class__.__name__.replace("Controller", "") + == target_controller + ), + None, + ) + return _controller + + def load_controllers(self): + # Initialize an empty list to store the imported controllers + self.controllers = [] + + # Construct the full directory path for controllers + controllers_directory = os.path.join("classes", "controllers") + + # Loop through files in the controllers directory + for filename in os.listdir(controllers_directory): + if filename.endswith("_controller.py"): + # Extract the module name without the extension + module_name = filename[:-3] + + # Construct the full module path + module_path = f"classes.controllers.{module_name}" + + # Import the module dynamically + module = importlib.import_module(module_path) + + # Check if the module defines a controller class and add it to the list + for name, obj in inspect.getmembers(module): + if ( + inspect.isclass(obj) + and issubclass(obj, Controller) + and obj != Controller + ): + # Create an instance of the controller + controller_instance = obj() ### inject notifier here + if not hasattr(controller_instance, "rendering_order"): + controller_instance.rendering_order = 0 + + self.controllers.append(controller_instance) + + self.controllers.sort(key=lambda controller: controller.rendering_order) + + for controller_instance in self.controllers: + if hasattr(controller_instance, "game_ready") and callable( + controller_instance.game_ready + ): + controller_instance.game_ready() diff --git a/classes/UI_controller.py b/classes/UI_controller.py deleted file mode 100644 index 490c786..0000000 --- a/classes/UI_controller.py +++ /dev/null @@ -1,63 +0,0 @@ -import pygame -from classes.Controller import Controller - - -class UIController(Controller): - def __init__(self, config): - super().__init__(config) - self.font_config = config.get("font_spritesheet_offsets") - self.ui_config = config.get("ui") - self.spritesheet = pygame.image.load("images/font_spritesheet.png") - self.canvas_width = 224 - self.canvas_height = 256 - self.canvas = pygame.Surface( - (self.canvas_width, self.canvas_height), pygame.SRCALPHA - ) - - self.get_score_callback = lambda: None - - def draw(self, surface): - surface.blit(self.canvas, (0, 0)) # blit the canvas onto the game surface - - def update(self, events, dt): - # clear the canvas between each draw - self.canvas.fill((0, 0, 0, 0)) - # get the score value using the callback to the scoreboard controller - score = str(self.get_score_callback()) - - # position SCORE text at position in config - self.canvas.blit( - self.create_text_surface(self.ui_config["score_label_text"]), - self.ui_config["score_label_position"], - ) - - # position SCORE value at position in config - text_surface = self.create_text_surface(score) - self.canvas.blit(text_surface, self.ui_config["score_value_position"]) - - # position HI-SCORE text at position in config - self.canvas.blit( - self.create_text_surface(self.ui_config["hiscore_label_text"]), - self.ui_config["hiscore_label_position"], - ) - - # position HI-SCORE value at position in config - text_surface = self.create_text_surface("00000") - self.canvas.blit(text_surface, self.ui_config["hiscore_value_position"]) - - return self - - def create_text_surface(self, text): - surface_width = len(text) * 8 - surface_height = 8 - text_surface = pygame.Surface((surface_width, surface_height), pygame.SRCALPHA) - text_surface.fill((0, 0, 0, 0)) - - for idx, letter in enumerate(text): - if letter in self.font_config: - letter_x, letter_y = self.font_config[letter] - letter_rect = pygame.Rect(letter_x, letter_y, 8, 8) - letter_image = self.spritesheet.subsurface(letter_rect) - text_surface.blit(letter_image, (idx * 8, 0)) - - return text_surface diff --git a/classes/bomb/Bomb_controller.py b/classes/bomb/Bomb_controller.py deleted file mode 100644 index c9d8302..0000000 --- a/classes/bomb/Bomb_controller.py +++ /dev/null @@ -1,120 +0,0 @@ -from classes.Controller import Controller -from classes.bomb.Bomb_factory import BombFactory -from classes.bomb.Bomb_container import BombContainer -from classes.invader.Invader import Invader -from classes.bomb.Bomb import Bomb -import random, pygame, os - - -class BombController(Controller): - def __init__(self, config): - self.counter = 0 - self.enabled = False - self.max_bombs = 1 - self.grace_period = 60 - - self.bomb_types = ["plunger", "squiggly", "rolling"] - self.bomb_factory = BombFactory(config) - self.bomb_container = BombContainer() - self.explode_bomb_image = pygame.image.load( - os.path.join("sprites", "invader_bomb", "bomb_exploding.png") - ) - - # callbacks defined in Game_controller - self.get_invaders_callback = None - self.get_player_callback = None - - # callback from game controller - def enable_bombs(self): - self.enabled = True - - # callback from game controller - def disable_bombs(self): - self.enabled = False - - def get_bombs(self): - return self.bomb_container.get_bombs() - - def on_play_delay_complete(self, data): - # print("swarm notify in bomb controller") - self.enabled = True - - def update(self, events, dt): - if self.get_invaders_callback: - if len(self.get_invaders_callback()) > 0 and self.enabled == True: - # create a new bomb - if len(self.bomb_container.get_bombs()) < self.max_bombs: - bomb = self.create_bomb() - if isinstance(bomb, Bomb): - self.bomb_container.add(bomb) - - # Update all existing bomb sprites in this container - self.counter += 1 - if self.counter == 3: - self.counter = 0 - for sprite in self.bomb_container: - sprite.update() - return self.bomb_container - - def create_bomb(self): - if self.bomb_container.has_rolling_shot(): - bomb_type = random.choice(self.bomb_types[:2]) - else: - bomb_type = random.choice(self.bomb_types) - - bomb_type = self.bomb_types[1] - - invader = self.find_attacking_invader(bomb_type) - if isinstance(invader, Invader): - return self.bomb_factory.create_bomb(invader, bomb_type) - - def find_attacking_invader(self, bomb_type): - invaders_with_clear_path = self.find_invaders_with_clear_path() - if len(invaders_with_clear_path) > 0: - if bomb_type == "rolling" and self.get_player_callback: - player_rect = self.get_player_callback().get_rect() - for invader in invaders_with_clear_path: - x1 = invader.rect.x + 8 - px1 = player_rect.x - px2 = px1 + 16 - if x1 >= px1 and x1 <= px2 and invader.active == True: - return invader - else: - return invaders_with_clear_path[ - random.randint(0, len(invaders_with_clear_path) - 1) - ] - - def find_invaders_with_clear_path(self): - invaders_with_clear_path = [] - invader_group = self.get_invaders_callback() - # find the lowest screen row (initially row 4) of remaining invaders - # invaders on this row number won't need a path check - max_row = max(invader_group, key=lambda invader: invader.row).row - - for invader in invader_group: - clear_path = True - - # if the invader is on the lowest screen row (highest row number) then don't check any further - if invader.row == max_row: - invaders_with_clear_path.append(invader) - # invader.image = pygame.image.load( - # "sprites/invader/invader-explode.png" - # ).convert_alpha() - continue - - # else begin inner loop: - # check all invaders against the invader in the outer loop - # if there is an invader with the same column (as the outer loop invader) - # but on a lower screen row (higher row number) then it's not a clear path - for _invader in invader_group: - if _invader.column == invader.column and _invader.row > invader.row: - clear_path = False - break - - if clear_path: - invaders_with_clear_path.append(invader) - # invader.image = pygame.image.load( - # "sprites/invader/invader-explode.png" - # ).convert_alpha() - - return invaders_with_clear_path diff --git a/classes/bomb/Bomb_factory.py b/classes/bomb/Bomb_factory.py deleted file mode 100644 index a530d18..0000000 --- a/classes/bomb/Bomb_factory.py +++ /dev/null @@ -1,29 +0,0 @@ -import pygame -from classes.bomb.Bomb import Bomb - - -class BombFactory: - def __init__(self, config): - self.config = config - self.bomb_frames = config.get("bombs")["images"] - - # Load bomb images using config into array - for bomb_type, frames in self.bomb_frames.items(): - loaded_frames = [] - for frame in frames: - image = pygame.image.load(self.config.get_file_path(frame)) - loaded_frames.append(image) - self.bomb_frames[bomb_type] = loaded_frames - - self.explode_frame = pygame.image.load( - "sprites/invader_bomb/bomb_exploding.png" - ).convert_alpha() - - def create_bomb(self, invader, bomb_type): - x = invader.rect.x + 7 - y = invader.rect.y + 8 - - bomb_sprite = Bomb( - x, y, self.bomb_frames[bomb_type], self.explode_frame, bomb_type - ) - return bomb_sprite diff --git a/classes/config/Audio_config.py b/classes/config/Audio_config.py new file mode 100644 index 0000000..7493c0e --- /dev/null +++ b/classes/config/Audio_config.py @@ -0,0 +1,11 @@ +import os +from classes.config.Base_config import BaseConfig + + +class AudioConfig(BaseConfig): + config_values = { + "mothership": "sounds/mothership.wav", + "mothership_bonus": "sounds/mothership_bonus.wav", + "player_explodes": "sounds/player_destroyed.wav", + "extra_life": "sounds/extra_life.wav", + } diff --git a/classes/config/Base_config.py b/classes/config/Base_config.py new file mode 100644 index 0000000..e702e08 --- /dev/null +++ b/classes/config/Base_config.py @@ -0,0 +1,14 @@ +import os + + +class BaseConfig: + @classmethod + def get(cls, key): + return cls.config_values.get(key) + + @classmethod + def get_file_path(cls, key): + base_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + parent_directory = os.path.dirname(base_directory) + file_path = os.path.join(parent_directory, key) + return file_path diff --git a/classes/config/Bomb_config.py b/classes/config/Bomb_config.py new file mode 100644 index 0000000..7d3e3fd --- /dev/null +++ b/classes/config/Bomb_config.py @@ -0,0 +1,8 @@ +from classes.config.Base_config import BaseConfig + + +class BombConfig(BaseConfig): + config_values = { + "max_bombs": 1, + "grace_period": 60, + } diff --git a/classes/config/Game_config.py b/classes/config/Game_config.py new file mode 100644 index 0000000..1a448bd --- /dev/null +++ b/classes/config/Game_config.py @@ -0,0 +1,12 @@ +import os +from classes.config.Base_config import BaseConfig + + +class GameConfig(BaseConfig): + config_values = { + "original_screen_size": (224, 256), + "larger_screen_size": (224 * 4, 256 * 4), + "bg_image_path": os.path.join("images", "invaders_moon_bg.png"), + "max_fps": 60, + "top_left": (0, 0), + } diff --git a/classes/config/Invader_config.py b/classes/config/Invader_config.py new file mode 100644 index 0000000..198c7f0 --- /dev/null +++ b/classes/config/Invader_config.py @@ -0,0 +1,18 @@ +from classes.config.Base_config import BaseConfig + + +class InvaderConfig(BaseConfig): + config_values = { + "spawn_rows": [128, 144, 160, 168, 168, 168, 176, 176, 176], + "points": [30, 20, 20, 10, 10], + "cols": 11, + "rows": 5, + "x_position_start": 16, + "x_repeat_offset": 16, + "y_repeat_offset": 17, + "screen_bottom_limit": 218, + "screen_left_limit": 25, + "screen_right_limit": 200, + "horizontal_move": 2, + "vertical_move": 8, + } diff --git a/classes/config/Mothership_config.py b/classes/config/Mothership_config.py new file mode 100644 index 0000000..7fa2e3e --- /dev/null +++ b/classes/config/Mothership_config.py @@ -0,0 +1,30 @@ +from classes.config.Base_config import BaseConfig + + +class MothershipConfig(BaseConfig): + config_values = { + # "cycles_with_explosion_frame": 60, + # "cycles_with_bonus_text": 60, + "cycles_until_spawn": 200, + "qualifying_invader_y_position": 36, + "spawn_left_position": (0, 48), + "spawn_right_position": (224, 48), + "points_table": [ + 50, + 50, + 50, + 50, + 50, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 150, + 150, + 300, + ], + } diff --git a/classes/config/Shield_config.py b/classes/config/Shield_config.py new file mode 100644 index 0000000..8aacbe7 --- /dev/null +++ b/classes/config/Shield_config.py @@ -0,0 +1,7 @@ +from classes.config.Base_config import BaseConfig + + +class ShieldConfig(BaseConfig): + config_values = { + "positions": [(32, 192), (77, 192), (122, 192), (167, 192)], + } diff --git a/classes/config/UI_config.py b/classes/config/UI_config.py new file mode 100644 index 0000000..31df555 --- /dev/null +++ b/classes/config/UI_config.py @@ -0,0 +1,22 @@ +from classes.config.Base_config import BaseConfig + + +class UIConfig(BaseConfig): + config_values = { + "score_label_text": "SCORE", + "score_label_position": (60, 16), + "score_value_position": (60, 32), + "hiscore_label_text": "HI-SCORE", + "hiscore_label_position": (118, 16), + "hiscore_value_position": (118, 32), + "game_over_position": (78, 48) + # "tints": [ + # {"position": (0, 0), "size": (224, 32), "color": (255, 255, 255)}, + # {"position": (0, 32), "size": (224, 32), "color": (255, 0, 0)}, + # {"position": (0, 64), "size": (224, 120), "color": (255, 255, 255)}, + # {"position": (0, 184), "size": (224, 56), "color": (0, 255, 0)}, + # {"position": (0, 240), "size": (24, 16), "color": (255, 255, 255)}, + # {"position": (24, 240), "size": (112, 16), "color": (0, 255, 0)}, + # {"position": (136, 240), "size": (88, 16), "color": (255, 255, 255)}, + # ], + } diff --git a/classes/bomb/Bomb_container.py b/classes/containers/Bomb_container.py similarity index 93% rename from classes/bomb/Bomb_container.py rename to classes/containers/Bomb_container.py index bb64260..83bfb04 100644 --- a/classes/bomb/Bomb_container.py +++ b/classes/containers/Bomb_container.py @@ -1,5 +1,5 @@ import pygame -from classes.bomb.Bomb import Bomb +from classes.models.Bomb import Bomb class BombContainer(pygame.sprite.Group): diff --git a/classes/invader/Invader_container.py b/classes/containers/Invader_container.py similarity index 64% rename from classes/invader/Invader_container.py rename to classes/containers/Invader_container.py index 40009b5..d429482 100644 --- a/classes/invader/Invader_container.py +++ b/classes/containers/Invader_container.py @@ -1,18 +1,20 @@ import pygame -from classes.invader.Invader_factory import InvaderFactory +from classes.config.Invader_config import InvaderConfig class InvaderContainer(pygame.sprite.Group): - def __init__(self, config): + def __init__(self): super().__init__() + + config = InvaderConfig() # copy the config values - self.invader_direction = config.get("invaders")["horizontal_move"] - self.invader_down_direction = config.get("invaders")["vertical_move"] - self.screen_left_limit = config.get("invaders")["screen_left_limit"] - self.screen_right_limit = config.get("invaders")["screen_right_limit"] - self.screen_bottom_limit = config.get("invaders")["screen_bottom_limit"] + self.invader_direction = config.get("horizontal_move") + self.invader_down_direction = config.get("vertical_move") + self.screen_left_limit = config.get("screen_left_limit") + self.screen_right_limit = config.get("screen_right_limit") + self.screen_bottom_limit = config.get("screen_bottom_limit") # when an invader reaches the edge of the screen this flag is set - self.invader_move_down = False + self.invaders_moving_down = False # used to track which invader in the group is next to move self.current_invader_index = 0 # flag used when invaders reach bottom of screen - game over @@ -28,8 +30,8 @@ def update(self): # remember last invader moves differently def handle_invader_movement(self): invader = self.get_invader_at_current_index() - if invader and invader.active == True: - if self.invader_move_down == False: + if invader and invader.active: + if not self.invaders_moving_down: invader.move_across(self.invader_direction) else: invader.move_down(self.invader_down_direction) @@ -47,14 +49,14 @@ def update_current_invader_index(self): self.update_movement_flags() def update_movement_flags(self): - if self.invader_move_down == True: - self.invader_move_down = False + if self.invaders_moving_down == True: + self.invaders_moving_down = False # check if any of the invaders have reached screen edge if self.has_reached_horizontal_limits(): # if so switch the direction and set the move_down flag self.invader_direction = self.invader_direction * -1 - self.invader_move_down = True + self.invaders_moving_down = True if self.has_reached_vertical_limit(): self.invaders_landed = True @@ -95,6 +97,35 @@ def get_invaders(self): def get_invader_count(self) -> int: return len(self.sprites()) + def get_invaders_with_clear_path(self): + invaders_with_clear_path = [] + invader_group = self.sprites() + # find the lowest screen row (initially row 4) of remaining invaders + # invaders on this row number won't need a path check + max_row = max(invader_group, key=lambda invader: invader.row).row + + for invader in invader_group: + clear_path = True + + # if the invader is on the lowest screen row (highest row number) then don't check any further + if invader.row == max_row: + invaders_with_clear_path.append(invader) + continue + + # else begin inner loop: + # check all invaders against the invader in the outer loop + # if there is an invader with the same column (as the outer loop invader) + # but on a lower screen row (higher row number) then it's not a clear path + for _invader in invader_group: + if _invader.column == invader.column and _invader.row > invader.row: + clear_path = False + break + + if clear_path: + invaders_with_clear_path.append(invader) + + return invaders_with_clear_path + def has_reached_vertical_limit(self) -> bool: for invader in self.sprites(): if invader.rect.y + invader.rect.height >= self.screen_bottom_limit: diff --git a/classes/controllers/Audio_controller.py b/classes/controllers/Audio_controller.py new file mode 100644 index 0000000..5ee78a7 --- /dev/null +++ b/classes/controllers/Audio_controller.py @@ -0,0 +1,45 @@ +import pygame +from lib.Controller import Controller +from classes.config.Audio_config import AudioConfig + +pygame.mixer.init() + + +class AudioController(Controller): + def __init__(self): + super().__init__() + config = AudioConfig() + # print(config.config_values) + self.mothership_sound = pygame.mixer.Sound(config.get("mothership")) + self.mothership_bonus_sound = pygame.mixer.Sound(config.get("mothership_bonus")) + self.player_explodes_sound = pygame.mixer.Sound(config.get("player_explodes")) + self.extra_life_sound = pygame.mixer.Sound(config.get("extra_life")) + + self.event_manager.add_listener( + "mothership_spawned", self.on_mothership_spawned + ) + self.event_manager.add_listener("mothership_hit", self.on_mothership_bonus) + + self.event_manager.add_listener("mothership_exit", self.on_mothership_exit) + self.event_manager.add_listener("player_explodes", self.on_player_explodes) + self.event_manager.add_listener("extra_life_awarded", self.on_extra_life) + + def on_mothership_spawned(self, data): + pass + # self.mothership_sound.play(-1) + + def on_extra_life(self, data): + self.extra_life_sound.play() + + def on_mothership_exit(self, data): + self.mothership_sound.fadeout(1000) + + def on_mothership_bonus(self, data): + self.mothership_sound.stop() + self.mothership_bonus_sound.play() + + def on_player_explodes(self, data): + self.player_explodes_sound.play() + + def play_player_shot_sound(self): + pass diff --git a/classes/controllers/Baseline_controller.py b/classes/controllers/Baseline_controller.py new file mode 100644 index 0000000..ea14e1b --- /dev/null +++ b/classes/controllers/Baseline_controller.py @@ -0,0 +1,44 @@ +import pygame, os +from lib.Controller import Controller + + +class BaselineController(Controller): + def __init__(self): + self.baselineSprite = pygame.sprite.Sprite() + + # create the surface for the sprite + self.baselineSprite.image = pygame.Surface((224, 1), pygame.SRCALPHA) + # define the green color as (R, G, B) tuple + self.baselineSprite.image.fill((0, 255, 0)) + + # Set the rect of the sprite + self.baselineSprite.rect = self.baselineSprite.image.get_rect() + self.baselineSprite.rect.x = 0 + self.baselineSprite.rect.y = 240 + + def draw(self, surface): + # draw the baselineSprite onto the specified surface + surface.blit(self.baselineSprite.image, self.baselineSprite.rect.topleft) + + def update(self, events, state): + bomb_callback = self.get_callback("get_bombs") + bomb_sprites = bomb_callback() + if len(bomb_sprites) > 0: + collisions = pygame.sprite.spritecollide( + self.baselineSprite, bomb_sprites, False + ) + if collisions: + for bomb_collision in collisions: + bomb_collision.rect.y = 233 + bomb_collision.active = False + masked_canvas = self.baselineSprite.image.copy() + masked_canvas.blit( + self.baselineImage, + # where to draw on the canvas. here its the x position of the bomb and 0 on the y + (bomb_collision.rect.x, 0), + special_flags=pygame.BLEND_RGBA_MULT, + ) + self.baselineSprite.image = masked_canvas + # bomb_collision.kill() + + return self diff --git a/classes/controllers/Bomb_controller.py b/classes/controllers/Bomb_controller.py new file mode 100644 index 0000000..9c37b64 --- /dev/null +++ b/classes/controllers/Bomb_controller.py @@ -0,0 +1,114 @@ +from lib.Controller import Controller + +from classes.factories.Bomb_factory import BombFactory +from classes.containers.Bomb_container import BombContainer +from classes.models.Invader import Invader +from classes.models.Bomb import Bomb +import random + + +class BombController(Controller): + def __init__(self): + super().__init__() + self.state = {} + + self.counter = 0 + self.enabled = False + self.max_bombs = 2 + self.grace_period = 60 + + self.bomb_types = ["plunger", "squiggly", "rolling"] + self.bomb_factory = BombFactory() + self.bomb_container = BombContainer() + + self.event_manager.add_listener("play_delay_complete", self.on_player_ready) + self.event_manager.add_listener("player_explodes", self.on_player_explodes) + + #self.register_callback("get_bombs", lambda: self.bomb_container.get_bombs()) + + self.get_invaders_callback = None + self.get_player_callback = None + self.get_invaders_clearpath_callback = None + + def game_ready(self): + pass + #self.get_invaders_callback = self.get_callback("get_invaders") + #self.get_player_callback = self.get_callback("get_player") + #self.get_invaders_clearpath_callback = self.get_callback( + # "get_invaders_with_clear_path" + #) + def get_bombs(self): + return self.bomb_container.get_bombs() + + def on_player_explodes(self, data): + self.enabled = False + + def on_player_ready(self, data): + self.enabled = True + + def get_surface(self): + return self.bomb_container + + def update(self, events, state): + self.state = state + invaders = self.get_invaders_callback() + + # if 'fire_button_pressed' in self.state: + # if self.state['fire_button_pressed']: + + if len(invaders) > 0: + if 'bombs_enabled' in self.state: + if self.state['bombs_enabled'] == True: + # create a new bomb + if len(self.bomb_container.get_bombs()) < self.max_bombs: + bomb = self.create_bomb() + if isinstance(bomb, Bomb): + self.bomb_container.add(bomb) + + # Update all existing bomb sprites in this container + self.counter += 1 + if self.counter == 3: + self.counter = 0 + for sprite in self.bomb_container: + sprite.update() + + def create_bomb(self): + def get_next_bomb_type(): + if self.bomb_container.has_rolling_shot(): + bomb_type = random.choice(self.bomb_types[:2]) + else: + bomb_type = random.choice(self.bomb_types) + + bomb_type = self.bomb_types[1] + return bomb_type + + bomb_type = get_next_bomb_type() + invader = self.find_attacking_invader(bomb_type) + if isinstance(invader, Invader): + return self.bomb_factory.create_bomb(invader, bomb_type) + + def find_attacking_invader(self, bomb_type): + invaders_with_clear_path = self.get_invaders_clearpath_callback() + + def is_valid_target(invader): + return invader.active + + def is_rolling_bomb(): + return bomb_type == "rolling" and self.get_player_callback is not None + + if is_rolling_bomb(): + player_rect = self.get_player_callback().get_rect() + valid_invaders = [ + invader + for invader in invaders_with_clear_path + if player_rect.x <= (invader.rect.x + 8) <= (player_rect.x + 16) + and is_valid_target(invader) + ] + if valid_invaders: + return random.choice(valid_invaders) + else: + if invaders_with_clear_path: + return random.choice(invaders_with_clear_path) + + # Return None if no valid invader is found + return None diff --git a/classes/Input_controller.py b/classes/controllers/Input_controller.py similarity index 77% rename from classes/Input_controller.py rename to classes/controllers/Input_controller.py index 1c31087..c3959e8 100644 --- a/classes/Input_controller.py +++ b/classes/controllers/Input_controller.py @@ -1,6 +1,5 @@ -import pygame from pygame.locals import * -from classes.Controller import Controller +from lib.Controller import Controller # could be configured with allowed keys @@ -8,13 +7,17 @@ # maybe the init method could have a mode which would define the keys it tracks # and it could be switched as well as set during config class InputController(Controller): - def __init__(self, config): - super().__init__(config) + def __init__(self): + super().__init__() - def update(self, events, dt): + def update(self, events, state): + # self.event_manager.notify("escape_button_pressed") + # def update(self, events, dt): for event in events: + # print("event", event) if event.type == KEYDOWN: if event.key == K_ESCAPE: + # print("escape press in input controller update") self.event_manager.notify("escape_button_pressed") if event.key == K_k: # 'K' key pressed diff --git a/classes/controllers/Invader_controller.py b/classes/controllers/Invader_controller.py new file mode 100644 index 0000000..a4f2562 --- /dev/null +++ b/classes/controllers/Invader_controller.py @@ -0,0 +1,112 @@ +from lib.Controller import Controller +from classes.factories.Invader_factory import InvaderFactory +from classes.containers.Invader_container import InvaderContainer + + +class InvaderController(Controller): + def __init__(self): + super().__init__() + + self.state = {} + + invader_factory = InvaderFactory() + self.invader_generator = invader_factory.create_invader_swarm() + self.invader_container = InvaderContainer() + self.swarm_complete = False + self.countdown = 0 + + + + # can we do this outside of the controller like in the state + # suppose we follow the react method of having access to state setting, + #self.event_manager.add_listener("invader_hit", self.on_invader_hit) + #self.event_manager.add_listener("player_explodes", self.on_player_explodes) + self.event_manager.add_listener("play_delay_complete", self.on_player_ready) + + # self.register_callback( + # "get_invaders", lambda: self.invader_container.get_invaders() + # ) + + # self.register_callback( + # "get_invaders_with_clear_path", + # lambda: self.invader_container.get_invaders_with_clear_path(), + # ) + + # self.register_callback( + # "get_invader_count", lambda: len(self.invader_container.get_invaders()) + # ) + + # self.register_callback( + # "get_lowest_invader_y", + # lambda: self.invader_container.get_invaders()[0].rect.y, + # ) + + # maybe this could be in base controller + # injects functions that can get state and change state + def set_up_state(self, fn_state_change, fn_get_state): + pass + + + # should this be in the state rather than controller + # this isn't core functionality and appears to be controlling state of vars + def game_restart(self): + invader_factory = InvaderFactory() + self.invader_generator = invader_factory.create_invader_swarm() + self.invader_container = InvaderContainer() + self.is_moving = False + self.swarm_complete = False + self.countdown = 0 + + def get_invaders(self): + return self.invader_container.get_invaders() + # suppose we get vars like coundown from the state object + # and vars like is_moving + # function get state set state + # like in react + def on_invader_hit(self, invader): + self.is_moving = False + # pause invaders 1/4 second (60/15) + self.countdown = 15 + invader.explode() + self.event_manager.notify("points_awarded", invader.points) + + def on_player_explodes(self, data): + self.is_moving = False + + def on_player_ready(self, data): + self.is_moving = True + + def generate_next_invader(self): + try: + self.invader_container.add_invader(next(self.invader_generator)) + except StopIteration: + if self.swarm_complete == False: + self.swarm_complete = True + self.is_moving = True + self.event_manager.notify("swarm_complete") + + def check_has_landed(): + pass + + def release_non_active(self): + self.invader_container.remove_inactive() + self.is_moving = True + self.event_manager.notify("invader_removed") + + def get_surface(self): + return self.invader_container + + # have another function that receives state? + def update(self, events, state): + self.state = state + self.is_moving = state['invaders_moving'] + if not self.swarm_complete: + self.generate_next_invader() + else: + if self.countdown > 0: + self.countdown -= 1 + if self.countdown <= 0: + self.release_non_active() + + if self.is_moving: + self.invader_container.update() diff --git a/classes/mothership/Mothership_controller.py b/classes/controllers/Mothership_controller.py similarity index 50% rename from classes/mothership/Mothership_controller.py rename to classes/controllers/Mothership_controller.py index 34a3a48..ecd01fe 100644 --- a/classes/mothership/Mothership_controller.py +++ b/classes/controllers/Mothership_controller.py @@ -1,23 +1,38 @@ import pygame -from classes.Controller import Controller -from classes.mothership.Mothership import Mothership +from lib.Controller import Controller +from classes.models.Mothership import Mothership +from classes.config.Mothership_config import MothershipConfig +from lib.Sprite_sheet import MothershipSpriteSheet class MothershipController(Controller): - def __init__(self, config): - super().__init__(config) - self.mothership_config = config.get("mothership") + def __init__(self): + super().__init__() + self.config = MothershipConfig() + self.sprite_sheet = MothershipSpriteSheet() + self.cycles_until_spawn = self.config.get("cycles_until_spawn") + self.spawn_right_position = self.config.get("spawn_right_position") + self.spawn_left_position = self.config.get("spawn_left_position") + self.qualifying_invader_y_position = self.config.get( + "qualifying_invader_y_position" + ) + self.points_table = self.config.get("points_table") + self.mothership_image = self.sprite_sheet.get_sprite("mothership_frame") + self.explode_image = self.sprite_sheet.get_sprite("explode_frame") + self.player_ready = False # set-up non-config related variables self.cycles_lapsed = 0 self.spawned = False self.shot_counter = 0 - # callbacks injected by game_controller - self.get_invader_count_callback = lambda: None - self.get_lowest_invader_y_callback = lambda: None - self.get_missile_callback = lambda: None - self.get_score_text_callback = lambda: None + self.register_callback("mothership_is_exploding", self.mothership_is_exploding) + self.event_manager.add_listener("player_explodes", self.on_player_explodes) + self.event_manager.add_listener("play_delay_complete", self.on_player_ready) + + self.event_manager.add_listener( + "fire_button_pressed", self.on_update_shot_counter + ) # mothership sprite stored in sprite group self.mothership_group = pygame.sprite.Group() @@ -28,7 +43,7 @@ def mothership_is_exploding(self): if self.mothership_group.sprites()[0].active == False: return True - def update(self, events, dt): + def update(self, events, state): if not self.spawned: self.update_spawn_logic() else: @@ -45,15 +60,16 @@ def update_mothership(self, dt): self.event_manager.notify("mothership_exit") def update_spawn_logic(self): - # if not self.check_invader_criteria(): - # return False + if not self.check_invader_criteria() or not self.check_player_criteria(): + return False self.cycles_lapsed += 1 - if self.cycles_lapsed == self.mothership_config["cycles_until_spawn"]: + if self.cycles_lapsed == self.cycles_until_spawn: self.spawn() def check_missile_collision(self): - missile = self.get_missile_callback() + missile_callback = self.get_callback("get_player_missile") + missile = missile_callback() mothership = self.mothership_group.sprites() if missile is not None and missile.active: collided = pygame.sprite.spritecollide(missile, mothership, False) @@ -64,8 +80,11 @@ def check_missile_collision(self): def mothership_hit(self): mothership = self.mothership_group.sprites()[0] points = mothership.calculate_points() - score_text_surface = self.get_score_text_callback(str(points)) - mothership.explode(score_text_surface) + + text_surface_callback = self.get_callback("get_score_text") + points_surface = text_surface_callback(str(points)) + + mothership.explode(points_surface) self.event_manager.notify("mothership_hit", points) def reset_spawn_state(self): @@ -73,17 +92,25 @@ def reset_spawn_state(self): self.cycles_lapsed = 0 self.spawned = False + def on_player_explodes(self, data): + self.player_ready = False + + def on_player_ready(self, data): + self.player_ready = True + def on_update_shot_counter(self, data): self.shot_counter = (self.shot_counter + 1) % 16 + def check_player_criteria(self): + return self.player_ready + def check_invader_criteria(self): - invader_count = self.get_invader_count_callback() - lowest_invader_y = self.get_lowest_invader_y_callback() + invader_count = self.get_callback("get_invader_count")() + lowest_invader_y = self.get_callback("get_lowest_invader_y")() return ( invader_count >= 8 - and lowest_invader_y - >= self.mothership_config["qualifying_invader_y_position"] + and lowest_invader_y >= self.qualifying_invader_y_position ) def get_spawn_direction(self): @@ -91,19 +118,20 @@ def get_spawn_direction(self): def get_spawn_position(self): return ( - self.mothership_config["spawn_right_position"] + self.spawn_right_position if self.shot_counter % 2 == 1 - else self.mothership_config["spawn_left_position"] + else self.spawn_left_position ) def spawn(self): self.event_manager.notify("mothership_spawned") self.spawned = True - print("Spawning mothership") self.mothership_group.add( Mothership( + self.mothership_image, + self.explode_image, self.get_spawn_position(), self.get_spawn_direction(), - self.mothership_config["points_table"], + self.points_table, ) ) diff --git a/classes/controllers/Player_controller.py b/classes/controllers/Player_controller.py new file mode 100644 index 0000000..beea1cd --- /dev/null +++ b/classes/controllers/Player_controller.py @@ -0,0 +1,88 @@ +import pygame +from lib.Controller import Controller +from classes.models.Player import Player + +player_speed = 1 + +class PlayerController(Controller): + def __init__(self): + super().__init__() + + self.state = {} + + self.spawned = False + self.is_exploding = False + self.exploding_animation_complete = False + + # player group has one player sprite + self.player_group_sprites = pygame.sprite.Group() + + + + def spawn_player(self): + self.spawned = True + params = { + "player_x_position": 10, + "player_y_position": 219, + } + player = Player(params) + self.player_group_sprites.add(player) + + + def explode_player(self): + print("exploding player") + self.is_exploding = True + self.get_player().explode() + + + def get_surface(self): + return self.player_group_sprites + + def update(self, events, state): + self.state = state + self.check_bomb_collisions() + if self.get_player(): + if 'player_enabled' in self.state: + if self.state['player_enabled'] == True: + self.update_player() + elif self.is_exploding: + self.get_player().update() + else: + if self.is_exploding: + print("end of explosion") + self.event_manager.notify("player_explosion_complete") + self.exploding_animation_complete = True + self.is_exploding = False + + + def update_player(self): + player = self.get_player() + if self.state['is_moving_left']: + player.rect.x -= player_speed + elif self.state['is_moving_right']: + player.rect.x += player_speed + player.update() + + def clamp(value, min_value, max_value): + return max(min(value, max_value), min_value) + + def get_player(self): + if self.player_group_sprites.sprites(): + return self.player_group_sprites.sprites()[0] + + def check_bomb_collisions(self): + if 'player_enabled' in self.state: + if self.state['player_enabled'] == True: + bomb_sprites = self.callback("get_bombs") + + if bomb_sprites is not None: + for bomb_sprite in bomb_sprites: + if bomb_sprite.active and pygame.sprite.collide_mask( + self.get_player(), bomb_sprite + ): + bomb_sprite.explode() + # print("here..") + # self.is_exploding = True + # self.player_enabled = False + # self.get_player().explode() + self.event_manager.notify("player_explodes") diff --git a/classes/controllers/Player_missile_controller.py b/classes/controllers/Player_missile_controller.py new file mode 100644 index 0000000..797e1d5 --- /dev/null +++ b/classes/controllers/Player_missile_controller.py @@ -0,0 +1,79 @@ +from lib.Controller import Controller +from classes.models.Player_missile import PlayerMissile +import pygame + + +class PlayerMissileController(Controller): + def __init__(self): + super().__init__() + self.state = {} + + self.ready_flag = False + self.rendering_order = 1 + # there will only ever be one sprite in this group + self.missile_group = pygame.sprite.Group() + + self.register_callback("get_player_missile", self.get_player_missile) + + self.event_manager.add_listener("invader_removed", self.on_missile_ready) + self.event_manager.add_listener("play_delay_complete", self.on_missile_ready) + + self.get_player_callback = None + self.get_invaders_callback = None + + + def game_ready(self): + return + + def on_missile_ready(self, data): + self.ready_flag = True + + def on_fire_pressed(self): + + + + + if ( + not self.missile_group + and self.ready_flag + and not self.callback("mothership_is_exploding") + ): + player = self.get_player_callback() #self.callback("get_player") + params = { + "player_x_position": player.rect.x, + "player_y_position": player.rect.y, + } + self.missile_group.add(PlayerMissile(params)) + + def check_invader_collisions(self): + if self.missile_group: + invaders = self.get_invaders_callback() #self.callback("get_invaders") + missile = self.get_player_missile() + for invader_sprite in invaders: + collision_area = pygame.sprite.collide_mask(invader_sprite, missile) + if collision_area is not None: + print("missile hit") + self.event_manager.notify("invader_hit", invader_sprite) + self.ready_flag = False + return True + + def update(self, events, state): + self.state = state + if 'fire_button_pressed' in self.state: + if self.state['fire_button_pressed']: + self.on_fire_pressed() + + if self.missile_group: + if not self.check_invader_collisions(): + self.get_player_missile().update() + else: + self.missile_group.remove(self.get_player_missile()) + + def get_surface(self): + return self.missile_group + + + # shields need access to player missile + def get_player_missile(self): + if self.missile_group: + return self.missile_group.sprites()[0] diff --git a/classes/controllers/Scoreboard_controller.py b/classes/controllers/Scoreboard_controller.py new file mode 100644 index 0000000..d34b4f3 --- /dev/null +++ b/classes/controllers/Scoreboard_controller.py @@ -0,0 +1,21 @@ +from lib.Controller import Controller + + +class ScoreboardController(Controller): + def __init__(self): + super().__init__() + self.score = 0 + self.update_ui_callback = lambda: None + + self.register_callback("get_score", self.get_score) + + self.event_manager.add_listener("mothership_hit", self.on_points_awarded) + self.event_manager.add_listener("points_awarded", self.on_points_awarded) + + def on_points_awarded(self, points): + self.score += points + if self.score >= 500 and self.callback("get_extra_life"): + self.event_manager.notify("extra_life_awarded") + + def get_score(self): + return str(self.score).zfill(5) diff --git a/classes/shield/Shield_controller.py b/classes/controllers/Shield_controller.py similarity index 54% rename from classes/shield/Shield_controller.py rename to classes/controllers/Shield_controller.py index 03e60a2..b324f92 100644 --- a/classes/shield/Shield_controller.py +++ b/classes/controllers/Shield_controller.py @@ -1,55 +1,45 @@ -from classes.Controller import Controller -from classes.shield.Shield_factory import ShieldFactory +from lib.Controller import Controller +from classes.factories.Shield_factory import ShieldFactory +from lib.Sprite_sheet import PlayerSpriteSheet import pygame class ShieldController(Controller): - def __init__(self, config): - super().__init__(config) - self.shield_container = ShieldFactory(config).create_shields() - self.explode_bomb_image = pygame.image.load( - "sprites/invader_bomb/bomb_exploding.png" - ) - self.missile_image_2x = pygame.image.load( - "sprites/player/player-shot-double-height.png" - ) - - # self.bomb_stem_image = pygame.image.load( - # "sprites/invader_bomb/explode-stem.png" - # ) + def __init__(self): + super().__init__() + self.rendering_order = -1 + self.shield_container = ShieldFactory().create_shields() - # callbacks defined in Game_controller - # if not injected they are still callable - self.get_bombs_callback = lambda: None - self.get_invaders_callback = lambda: None - self.get_missile_callback = lambda: None + self.get_invaders_callback = None + self.get_bombs_callback = None + self.get_player_missile_callback = None - def update(self, events, dt): - self.check_bomb_collisions() - self.check_invader_collision() - self.check_missile_collision() - return self.shield_container + # place any code here that should run when controllers have loaded + def game_ready(self): + return - def check_missile_collision(self): - missile = self.get_missile_callback() + def get_surface(self): + return self.shield_container - if missile is not None and missile.active: - _missile = pygame.sprite.Sprite() # Create an instance of the Sprite class - _missile.rect = missile.rect.copy() # Copy the rectangle - # because the missile moves up 4 pixels each cycle - # we need a sprite 2x height to ensure that sprite collision - # doesn't miss any pixels between position jumps - _missile.image = self.missile_image_2x # Copy the image - # _missile.rect.y -= 1 # Adjust the copied missile's position + def update(self, events, state): + pass + # get_invaders_callback = self.get_callback("get_invaders") + # print(get_invaders_callback) + #self.check_bomb_collisions() + #self.check_invader_collision() + #self.check_missile_collision() + # return self.shield_container - for shield_sprite in self.shield_container: - if pygame.sprite.collide_mask(shield_sprite, _missile): - missile.explode(missile.rect.move(-4, -3)) - # self.rect.x -= 4 - # self.rect.y -= 3 + def check_missile_collision(self): + missile = self.get_player_missile_callback + print(self.get_player_missile_callback) - shield_sprite.missile_damage(missile) - # self.event_manager.notify("missile_collision", shield_sprite) + # if missile is not None and missile.active: + # for shield_sprite in self.shield_container: + # collision_area = pygame.sprite.collide_mask(shield_sprite, missile) + # if collision_area is not None: + # shield_sprite.missile_collision(collision_area) + # missile.explode((-4, 2)) def check_invader_collision(self): invaders = self.get_invaders_callback() @@ -63,14 +53,14 @@ def check_invader_collision(self): def check_bomb_collisions(self): bomb_sprites = self.get_bombs_callback() - if bomb_sprites is not None: + if bomb_sprites: for shield_sprite in self.shield_container: for bomb_sprite in bomb_sprites: if bomb_sprite.active and pygame.sprite.collide_mask( shield_sprite, bomb_sprite ): bomb_sprite.explode() - shield_sprite.bomb_damage(bomb_sprite) + shield_sprite.bomb_collision(bomb_sprite) # if self.get_bombs_callback: # bomb_sprites = self.get_bombs_callback() diff --git a/classes/controllers/UI_controller.py b/classes/controllers/UI_controller.py new file mode 100644 index 0000000..a1e12db --- /dev/null +++ b/classes/controllers/UI_controller.py @@ -0,0 +1,107 @@ +import pygame +from lib.Controller import Controller +from classes.config.UI_config import UIConfig +from lib.Sprite_sheet import FontSpriteSheet +from lib.Sprite_sheet import PlayerSpriteSheet + + +class UIController(Controller): + CANVAS_HEIGHT_PLAYER_LIVES = 8 + CANVAS_HEIGHT = 256 + CANVAS_WIDTH = 224 + + def __init__(self): + super().__init__() + self.config = UIConfig() + self.canvas_width = self.CANVAS_WIDTH + self.canvas_height = self.CANVAS_HEIGHT + self.canvas = pygame.Surface( + (self.canvas_width, self.canvas_height), pygame.SRCALPHA + ) + self.sprite_sheet = FontSpriteSheet() + self.player_sprite_sheet = PlayerSpriteSheet() + self.register_callback("get_score_text", self.create_text_surface) + + def text_generator(self, text_to_display): + time_between_chars = 10 # Delay frames between each character + char_index = 0 + frame_count = 0 + current_text = "" + + while char_index < len(text_to_display): + if frame_count >= time_between_chars: + current_text = text_to_display[: char_index + 1] + yield current_text + char_index += 1 + frame_count = 0 + else: + yield current_text # Return the progressively increasing text during the delay + frame_count += 1 + + def draw_lives(self): + lives = self.callback("get_lives_count") + player_base = self.player_sprite_sheet.get_sprite("player") + # create a canvas the size of players + if lives > 0: + lives_canvas = pygame.Surface( + (17 * lives, self.CANVAS_HEIGHT_PLAYER_LIVES), pygame.SRCALPHA + ) + for i in range(lives - 1): + lives_canvas.blit(player_base, (i * 16 + 12, 0)) + else: + lives_canvas = pygame.Surface( + (17, self.CANVAS_HEIGHT_PLAYER_LIVES), pygame.SRCALPHA + ) + + lives_remaining_canvas = self.create_text_surface(str(lives)) + lives_canvas.blit(lives_remaining_canvas, (0, 0)) + + return lives_canvas + + def draw(self, surface): + surface.blit(self.canvas, (0, 0)) # blit the canvas onto the game surface + + def update(self, events, state): + # clear the canvas between each draw + self.canvas.fill((0, 0, 0, 0)) + + # get the score value using the callback to the scoreboard controller + score = self.callback("get_score") + + # lives_canvas = self.draw_lives() + # self.canvas.blit(lives_canvas, (1, 242)) + + # position SCORE text at position in config + self.canvas.blit( + self.create_text_surface(self.config.get("score_label_text")), + self.config.get("score_label_position"), + ) + + # position SCORE value at position in config + text_surface = self.create_text_surface(score) + self.canvas.blit(text_surface, self.config.get("score_value_position")) + + # position HI-SCORE text at position in config + self.canvas.blit( + self.create_text_surface(self.config.get("hiscore_label_text")), + self.config.get("hiscore_label_position"), + ) + + # position HI-SCORE value at position in config + text_surface = self.create_text_surface("00000") + self.canvas.blit(text_surface, self.config.get("hiscore_value_position")) + + return self + + def create_text_surface(self, text): + surface_width = len(text) * 8 + surface_height = 8 + text_surface = pygame.Surface((surface_width, surface_height), pygame.SRCALPHA) + text_surface.fill((0, 0, 0, 0)) + + for idx, letter in enumerate(text): + if not letter == " ": + char_image = self.sprite_sheet.get_sprite(letter) + text_surface.blit(char_image, (idx * 8, 0)) + + return text_surface diff --git a/classes/controllers/UI_game_over_controller.py b/classes/controllers/UI_game_over_controller.py new file mode 100644 index 0000000..0d1ea66 --- /dev/null +++ b/classes/controllers/UI_game_over_controller.py @@ -0,0 +1,38 @@ +from classes.controllers.UI_controller import UIController + + +class UIGameOverController(UIController): + def __init__(self): + super().__init__() + + self.game_over_message_position = (78, 48) + self.game_over_text = "GAME OVER" + + self.game_over_text_iterator = self.text_generator(self.game_over_text) + self.game_over_text_running = False + + self.event_manager.add_listener("game_ended", self.on_game_over) + self.pause = 0 + + def on_game_over(self, data): + print("on game over") + self.game_over_text_running = True + + def update(self, events, state): + self.canvas.fill((0, 0, 0, 0)) + + if self.game_over_text_running: + try: + current_text = next(self.game_over_text_iterator) + except StopIteration: + current_text = self.game_over_text + + self.pause += 1 + if self.pause == 180: + self.event_manager.notify("game_over_animation_ended") + self.game_over_text_running = False + + text_surface = self.create_text_surface(current_text) + self.canvas.blit(text_surface, self.config.get("game_over_position")) + + return self diff --git a/classes/factories/Bomb_factory.py b/classes/factories/Bomb_factory.py new file mode 100644 index 0000000..bea890d --- /dev/null +++ b/classes/factories/Bomb_factory.py @@ -0,0 +1,39 @@ +from classes.models.Bomb import Bomb +from lib.Sprite_sheet import BombSpriteSheet + + +class BombFactory: + def __init__(self): + sprite_sheet = BombSpriteSheet() + + self.bomb_frames = {} + self.bomb_frames["squiggly"] = [ + sprite_sheet.get_sprite("squiggly_frame1"), + sprite_sheet.get_sprite("squiggly_frame2"), + sprite_sheet.get_sprite("squiggly_frame3"), + sprite_sheet.get_sprite("squiggly_frame4"), + ] + + self.bomb_frames["rolling"] = [ + sprite_sheet.get_sprite("rolling_frame1"), + sprite_sheet.get_sprite("rolling_frame2"), + sprite_sheet.get_sprite("rolling_frame3"), + sprite_sheet.get_sprite("rolling_frame4"), + ] + + self.bomb_frames["plunger"] = [ + sprite_sheet.get_sprite("plunger_frame1"), + sprite_sheet.get_sprite("plunger_frame2"), + sprite_sheet.get_sprite("plunger_frame3"), + sprite_sheet.get_sprite("plunger_frame4"), + ] + + self.exploding_frame = sprite_sheet.get_sprite("explode_frame") + + def create_bomb(self, invader, bomb_type): + x, y = invader.bomb_launch_position() + + bomb_sprite = Bomb( + x, y, self.bomb_frames[bomb_type], self.exploding_frame, bomb_type + ) + return bomb_sprite diff --git a/classes/factories/Invader_factory.py b/classes/factories/Invader_factory.py new file mode 100644 index 0000000..3f21ab5 --- /dev/null +++ b/classes/factories/Invader_factory.py @@ -0,0 +1,88 @@ +from classes.models.Invader import Invader +from classes.config.Invader_config import InvaderConfig +from lib.Sprite_sheet import InvaderSpriteSheet + + +class InvaderFactory: + def __init__(self): + config = InvaderConfig() + + self.points_array = config.get("points") + self.spawn_rows = config.get("spawn_rows") + + self.invader_cols = config.get("cols") + + # number of invaders to draw vertically + self.invader_rows = config.get("rows") + + # starting x position of invaders + self.x_position_start = config.get("x_position_start") + + # horizontal offset between each invader + self.x_repeat_offset = config.get("x_repeat_offset") + + # vertical offset between each invader + self.y_repeat_offset = config.get("y_repeat_offset") + + sprite_sheet = InvaderSpriteSheet() + + self.explode_image = sprite_sheet.get_sprite("invader_explode_frame") + # invaders build/drawn upwards on screen + + self.invader_build_array = [ + [ + sprite_sheet.get_sprite("invader_small_frame1"), + sprite_sheet.get_sprite("invader_small_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_small_frame1"), + sprite_sheet.get_sprite("invader_small_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_mid_frame1"), + sprite_sheet.get_sprite("invader_mid_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_large_frame1"), + sprite_sheet.get_sprite("invader_large_frame2"), + ], + [ + sprite_sheet.get_sprite("invader_large_frame1"), + sprite_sheet.get_sprite("invader_large_frame2"), + ], + ] + + def create_invader_swarm(self): + spawn_rows_pointer = 0 + + # starting y position of invaders (change with each wave cleared) + y_position_start = self.spawn_rows[spawn_rows_pointer] + index = 0 + + for y_position in range(self.invader_rows): + for x_position in range(self.invader_cols): + yield self.create_invader( + self.x_position_start + (x_position * self.x_repeat_offset), + y_position_start - (y_position * self.y_repeat_offset), + True, + x_position, + 4 - y_position, + index, + self.points_array[4 - y_position], + ) + + index += 1 + + def create_invader(self, x, y, active, column, row, index, points): + invader_sprite = Invader( + x, + y, + active, + column, + row, + self.invader_build_array[row], + self.explode_image, + index, + points, + ) + return invader_sprite diff --git a/classes/shield/Shield_factory.py b/classes/factories/Shield_factory.py similarity index 50% rename from classes/shield/Shield_factory.py rename to classes/factories/Shield_factory.py index ee1ac3f..2817919 100644 --- a/classes/shield/Shield_factory.py +++ b/classes/factories/Shield_factory.py @@ -1,12 +1,13 @@ import pygame -from classes.shield.Shield import Shield +from classes.models.Shield import Shield +from classes.config.Shield_config import ShieldConfig +from lib.Sprite_sheet import ShieldSpriteSheet class ShieldFactory: - def __init__(self, config): - image = config.get("shields")["image"] - self.shield_image = pygame.image.load(config.get_file_path(image)) - self.shield_positions = config.get("shields")["positions"] + def __init__(self): + self.shield_image = ShieldSpriteSheet().get_sprite("shield_frame") + self.shield_positions = ShieldConfig().get("positions") def create_shields(self): sprite_group = pygame.sprite.Group() diff --git a/classes/invader/Invader.py b/classes/invader/Invader.py deleted file mode 100644 index 1cc6e51..0000000 --- a/classes/invader/Invader.py +++ /dev/null @@ -1,63 +0,0 @@ -import pygame.sprite - - -class Invader(pygame.sprite.Sprite): - def __init__(self, x, y, active, column, row, image_frames, index, points): - super().__init__() - - self.index = index - self.row = row - self.column = column - self.frame_pointer = 0 - self.image_frames = image_frames - self.active = active - self.points = points - - self.empty_frame = pygame.image.load( - "sprites/invader/invader-empty-frame.png" - ).convert_alpha() - - self.explode_frame = pygame.image.load( - "sprites/invader/invader-explode.png" - ).convert_alpha() - - # Create copies of the image frames to modify without affecting originals - self.modified_frames = [frame.copy() for frame in self.image_frames] - - self.image = self.modified_frames[0] # Start with the modified copy - self.rect = self.image.get_rect(topleft=(x, y)) - - def modify_pixel_colors(self): - green = (0, 255, 0) - white = (255, 255, 255) - - for frame in self.modified_frames: - for y in range(frame.get_height()): - for x in range(frame.get_width()): - pixel_color = frame.get_at((x, y)) - if y + self.rect.y >= 191: - pixel_color.r, pixel_color.g, pixel_color.b = green - else: - pixel_color.r, pixel_color.g, pixel_color.b = white - - frame.set_at((x, y), pixel_color) - - def explode(self): - self.image = self.explode_frame - self.active = False - - def release(self): - self.kill() - - def move_across(self, direction): - self.image = self.get_sprite_image() - self.rect.x += direction - - def move_down(self, direction): - self.image = self.get_sprite_image() - self.rect.y += direction - self.modify_pixel_colors() - - def get_sprite_image(self): - self.frame_pointer = 1 - self.frame_pointer - return self.modified_frames[self.frame_pointer] # Use the modified copy diff --git a/classes/invader/Invader_controller.py b/classes/invader/Invader_controller.py deleted file mode 100644 index 1e3c15d..0000000 --- a/classes/invader/Invader_controller.py +++ /dev/null @@ -1,71 +0,0 @@ -from classes.Controller import Controller -from classes.invader.Invader_factory import InvaderFactory -from classes.invader.Invader_container import InvaderContainer - - -class InvaderController(Controller): - def __init__(self, config): - super().__init__(config) - invader_factory = InvaderFactory(config) - self.invader_generator = invader_factory.create_invader_swarm() - self.invader_container = InvaderContainer(config) - self.is_moving = False - self.swarm_complete = False - self.countdown = 0 - - # callbacks defined in Game_controller - self.pause_player_missile_callback = None - self.resume_player_missile_callback = None - - def generate_next_invader(self): - try: - self.invader_container.add_invader(next(self.invader_generator)) - except StopIteration: - if self.swarm_complete == False: - self.swarm_complete = True - self.is_moving = True - self.event_manager.notify("swarm_complete") - - def get_lowest_invader_y(self): - return self.invader_container.get_invaders()[0].rect.y - - def get_invader_count(self): - return len(self.invader_container.get_invaders()) - - def get_invaders(self): - return self.invader_container.get_invaders() - - def stop_movement(self): - self.is_moving = False - - def start_movement(self): - self.is_moving = True - - def check_has_landed(): - pass - - def on_invader_hit(self, invader): - self.stop_movement() - # # pause invaders 1/4 second (60/15) - self.countdown = 15 - invader.explode() - self.event_manager.notify("points_awarded", invader.points) - - def release_non_active(self): - self.invader_container.remove_inactive() - self.start_movement() - self.event_manager.notify("invader_removed") - - def update(self, events, dt): - if not self.swarm_complete: - self.generate_next_invader() - else: - if self.countdown > 0: - self.countdown -= 1 - if self.countdown <= 0: - self.release_non_active() - - if self.is_moving: - self.invader_container.update() - - return self.invader_container diff --git a/classes/invader/Invader_factory.py b/classes/invader/Invader_factory.py deleted file mode 100644 index 1bc29c2..0000000 --- a/classes/invader/Invader_factory.py +++ /dev/null @@ -1,77 +0,0 @@ -import pygame -from classes.invader.Invader import Invader # , ModifiedInvader - - -class InvaderFactory: - def __init__(self, config): - self.config = config - invader_frames = config.get("invaders")["images"] - - # Load images and update invader_frames dictionary - for invader_size, frames in invader_frames.items(): - loaded_frames = [] - for frame in frames: - image = pygame.image.load(self.config.get_file_path(frame)) - loaded_frames.append(image) - invader_frames[invader_size] = loaded_frames - - # invaders build from bottom up - self.invader_build_array = [ - invader_frames["small"], - invader_frames["mid"], - invader_frames["mid"], - invader_frames["large"], - invader_frames["large"], - ] - - def create_invader_swarm(self): - spawn_rows_pointer = 0 - spawn_rows = self.config.get("invaders")["spawn_rows"] - - # number of invaders to draw horizontally - invader_cols = self.config.get("invaders")["cols"] - - # number of invaders to draw vertically - invader_rows = self.config.get("invaders")["rows"] - - # starting x position of invaders - x_position_start = self.config.get("invaders")["x_position_start"] - - # horizontal offset between each invader - x_repeat_offset = self.config.get("invaders")["x_repeat_offset"] - - # vertical offset between each invader - y_repeat_offset = self.config.get("invaders")["y_repeat_offset"] - - # starting y position of invaders (change with each wave cleared) - y_position_start = spawn_rows[spawn_rows_pointer] - index = 0 - - for y_position in range(invader_rows): - for x_position in range(invader_cols): - yield self.create_invader( - x_position_start + (x_position * x_repeat_offset), - y_position_start - (y_position * y_repeat_offset), - True, - x_position, - 4 - y_position, - index, - ) - - index += 1 - - def create_invader(self, x, y, active, column, row, index): - points_array = self.config.get("invaders")["points"] - points_for_row = points_array[row] - # points = self.config.get("invaders")[row] - invader_sprite = Invader( - x, - y, - active, - column, - row, - self.invader_build_array[row], - index, - points_for_row, - ) - return invader_sprite diff --git a/classes/bomb/Bomb.py b/classes/models/Bomb.py similarity index 63% rename from classes/bomb/Bomb.py rename to classes/models/Bomb.py index 3254808..4948844 100644 --- a/classes/bomb/Bomb.py +++ b/classes/models/Bomb.py @@ -1,7 +1,7 @@ -import pygame.sprite +from lib.Game_sprite import GameSprite -class Bomb(pygame.sprite.Sprite): +class Bomb(GameSprite): def __init__(self, x, y, image_frames, explode_frame, bomb_type): super().__init__() self.image_frames = image_frames @@ -20,21 +20,6 @@ def __init__(self, x, y, image_frames, explode_frame, bomb_type): self.rect.x = x self.rect.y = y - def modify_pixel_colors(self): - green = (0, 255, 0) - white = (255, 255, 255) - - for frame in self.modified_frames: - for y in range(frame.get_height()): - for x in range(frame.get_width()): - pixel_color = frame.get_at((x, y)) - if y + self.rect.y >= 191: - pixel_color.r, pixel_color.g, pixel_color.b = green - else: - pixel_color.r, pixel_color.g, pixel_color.b = white - - frame.set_at((x, y), pixel_color) - def explode(self): self.image = self.explode_frame @@ -58,14 +43,13 @@ def update(self): if self.rect.y <= 233: self.rect.y += 2 * 1.4 # 3 * 1.4 - self.modify_pixel_colors() - # if self.rect.y > 232: - # self.rect.y = 232 - # # self.kill() + if self.rect.y > 232: + self.rect.y = 232 + self.kill() else: self.countdown -= 1 if self.countdown == 0: self.kill() def get_sprite_image(self): - return self.modified_frames[self.frame_pointer] # Use the modified copy + return self.modify_pixel_colors(self.modified_frames[self.frame_pointer]) diff --git a/classes/models/Invader.py b/classes/models/Invader.py new file mode 100644 index 0000000..c9ed699 --- /dev/null +++ b/classes/models/Invader.py @@ -0,0 +1,47 @@ +from lib.Game_sprite import GameSprite + + +class Invader(GameSprite): + def __init__( + self, x, y, active, column, row, image_frames, explode_image, index, points + ): + super().__init__() + + self.index = index + self.row = row + self.column = column + self.frame_pointer = 0 + self.image_frames = image_frames + self.explode_frame = explode_image + self.active = active + self.points = points + + # Create copies of the image frames to modify without affecting originals + self.modified_frames = [frame.copy() for frame in self.image_frames] + + self.image = self.modified_frames[0] # Start with the modified copy + self.rect = self.image.get_rect(topleft=(x, y)) + + def bomb_launch_position(self): + return (self.rect.x + 7, self.rect.y + 8) + + def explode(self): + self.image = self.explode_frame + self.active = False + + def release(self): + self.kill() + + def move_across(self, direction): + self.image = self.get_sprite_image() + self.rect.x += direction + + def move_down(self, direction): + self.rect.y += direction + self.image = self.get_sprite_image() + + def get_sprite_image(self): + self.frame_pointer = 1 - self.frame_pointer + return self.modify_pixel_colors( + self.modified_frames[self.frame_pointer] + ) # Use the modified copy diff --git a/classes/mothership/Mothership.py b/classes/models/Mothership.py similarity index 52% rename from classes/mothership/Mothership.py rename to classes/models/Mothership.py index 1daf561..789476b 100644 --- a/classes/mothership/Mothership.py +++ b/classes/models/Mothership.py @@ -1,13 +1,14 @@ -import pygame.sprite +from lib.Game_sprite import GameSprite -class Mothership(pygame.sprite.Sprite): - def __init__(self, spawn_position, direction, points_table): +class Mothership(GameSprite): + def __init__( + self, mothership_image, explode_image, spawn_position, direction, points_table + ): super().__init__() - self.sprite_path = "sprites/mothership/mothership.png" - self.sprite_path_explode = "sprites/mothership/mothership-exploding.png" self.points_table = points_table - self.image = pygame.image.load(self.sprite_path).convert_alpha() + self.image = mothership_image + self.explode_image = explode_image self.rect = self.image.get_rect(topleft=spawn_position) self.direction = direction self.shot_counter = 0 @@ -20,7 +21,7 @@ def explode(self, score_text_surface): self.rect.x = min(self.rect.x, 208) self.active = False self.points_image = score_text_surface - self.image = pygame.image.load(self.sprite_path_explode).convert_alpha() + self.image = self.explode_image def update(self, shot_counter, dt): self.shot_counter = shot_counter @@ -31,7 +32,7 @@ def update(self, shot_counter, dt): return self def update_move(self, dt): - self.rect.x += self.direction * dt + self.rect.x += self.direction * 1 if self.has_reached_screen_edge(): self.kill() @@ -52,22 +53,4 @@ def has_reached_screen_edge(self): ) def draw(self, surface): - self.modify_pixel_colors() - surface.blit(self.image, self.rect) - - # in the arcade game a red filter was applied - def modify_pixel_colors(self): - red = (255, 0, 0) - white = (255, 255, 255) - for y in range(self.image.get_height()): - for x in range(self.image.get_width()): - pixel_color = self.image.get_at((x, y)) - if ( - pixel_color[0] == 255 - and pixel_color[1] == 255 - and pixel_color[2] == 255 - ): - # print("all white") - # print(pixel_color) - pixel_color.r, pixel_color.g, pixel_color.b = red - self.image.set_at((x, y), pixel_color) + surface.blit(self.modify_pixel_colors(self.image), self.rect) diff --git a/classes/models/Player.py b/classes/models/Player.py new file mode 100644 index 0000000..a9366ba --- /dev/null +++ b/classes/models/Player.py @@ -0,0 +1,55 @@ +from lib.Game_sprite import GameSprite +from lib.Sprite_sheet import PlayerSpriteSheet + + +class Player(GameSprite): + ANIMATION_FRAME_THRESHOLD_LOW = 5 + ANIMATION_FRAME_THRESHOLD_HIGH = 10 + MAX_ANIMATION_COUNT = 6 + + def __init__(self, params): + super().__init__() + + self.explosion_frame_number = 0 + self.explosion_animation_count = 0 + + self.is_exploding = False + self.sprite_sheet = PlayerSpriteSheet() + self.image = self.sprite_sheet.get_sprite("player") + self.exploding_images = [ + self.sprite_sheet.get_sprite("player_explode1"), + self.sprite_sheet.get_sprite("player_explode2"), + ] + self.rect = self.image.get_rect( + x=params.get("player_x_position"), y=params.get("player_y_position") + ) + + def update(self): + if self.is_exploding: + return self.update_exploding() + else: + return self + + def explode(self): + self.is_exploding = True + self.image = self.exploding_images[0] + + def update_exploding(self): + if self.explosion_animation_count < self.MAX_ANIMATION_COUNT: + self.explosion_frame_number += 1 + if self.explosion_frame_number == self.ANIMATION_FRAME_THRESHOLD_LOW: + self.image = self.exploding_images[1] + elif self.explosion_frame_number == self.ANIMATION_FRAME_THRESHOLD_HIGH: + self.image = self.exploding_images[0] + self.explosion_frame_number = 0 + self.explosion_animation_count += 1 + return self + else: + self.kill() + + # used by BombController + def get_rect(self): + return self.rect + + def draw(self, surface): + surface.blit(self.modify_pixel_colors(self.image), self.rect) diff --git a/classes/models/Player_missile.py b/classes/models/Player_missile.py new file mode 100644 index 0000000..43ff13a --- /dev/null +++ b/classes/models/Player_missile.py @@ -0,0 +1,45 @@ +from lib.Game_sprite import GameSprite +from lib.Sprite_sheet import PlayerSpriteSheet + + +class PlayerMissile(GameSprite): + def __init__(self, params): + super().__init__() + + sprite_sheet = PlayerSpriteSheet() + self.image = sprite_sheet.get_sprite("missile") + self.explode_frame = sprite_sheet.get_sprite("missile_explode") + + self.countdown = 0 + self.active = True + self.rect = self.image.get_rect( + x=params.get("player_x_position") + 8, y=params.get("player_y_position") + ) + + def draw(self, surface): + # surface.blit(self.image, self.rect) + surface.blit(self.modify_pixel_colors(self.image), self.rect) + + def remove(self): + self.kill() + + def explode(self, offset_rect=None): + if self.active: + self.image = self.explode_frame + if offset_rect: + self.rect = self.rect.move(offset_rect) + + self.active = False + self.countdown = 15 + + def update(self): + if self.countdown > 0: + self.countdown -= 1 + if self.countdown <= 0: + self.kill() + else: + self.rect.y -= 4 # Move the missile vertically upwards + if self.rect.y <= 42: + self.explode(()) + + return self diff --git a/classes/shield/Shield.py b/classes/models/Shield.py similarity index 78% rename from classes/shield/Shield.py rename to classes/models/Shield.py index 7f4896e..d4523ee 100644 --- a/classes/shield/Shield.py +++ b/classes/models/Shield.py @@ -1,7 +1,11 @@ +import random import pygame +from lib.Game_sprite import GameSprite +from lib.Sprite_sheet import PlayerSpriteSheet -class Shield(pygame.sprite.Sprite): + +class Shield(GameSprite): def __init__(self, x, y, image): super().__init__() @@ -12,39 +16,30 @@ def __init__(self, x, y, image): self.rect.y = y # create the mask from the shield image # used in collision detection + self.modify_pixel_colors(self.image) self.mask = pygame.mask.from_surface(self.image) + self.missile_explode_frame = PlayerSpriteSheet().get_sprite("missile_explode") - def missile_damage(self, missile_sprite): - shield_rect = self.rect - - global_position = missile_sprite.rect.topleft - # print(global_position) - new_y = global_position[1] - 0 - new_x = global_position[0] - 0 - - # Create a new tuple with the modified y-position - global_position = (new_x, new_y) + def missile_collision(self, collision_area): + modified_shield_surface = self.image.copy() - # print(global_position) - # Convert global position to local position inside the shield - local_position = ( - global_position[0] - shield_rect.x, - global_position[1] - shield_rect.y, - ) + collision_rect = pygame.Rect(collision_area[0], collision_area[1], 0, 0) + # update collision to align with preferred shield damage area + collision_rect = collision_rect.move((-4, -2)) - modified_shield_surface = self.image.copy() modified_shield_surface.blit( - missile_sprite.explode_frame, - # (local_position[0], 0 - y_adjust), - (local_position[0], local_position[1]), + self.missile_explode_frame, + collision_rect, special_flags=pygame.BLEND_RGBA_SUB, ) + self.image = modified_shield_surface - # update the sprite mask so future collisions - # use the mask rather than a basic rect + # pygame.time.delay(1000) + + # update the sprite mask for future collisions self.mask = pygame.mask.from_surface(modified_shield_surface) - def bomb_damage(self, bomb_sprite): + def bomb_collision(self, bomb_sprite): shield_rect = self.rect global_position = bomb_sprite.rect.topleft @@ -53,7 +48,6 @@ def bomb_damage(self, bomb_sprite): global_position[0] - shield_rect.x, global_position[1] - shield_rect.y, ) - # print(local_position) bomb_type = bomb_sprite.bomb_type # if bomb_type == "plunger": @@ -65,7 +59,7 @@ def bomb_damage(self, bomb_sprite): modified_shield_surface = self.image.copy() modified_shield_surface.blit( - bomb_sprite.explode_frame, + self.modify_pixel_colors(bomb_sprite.explode_frame), # (local_position[0], 0 - y_adjust), (local_position[0], local_position[1]), special_flags=pygame.BLEND_RGBA_SUB, @@ -73,7 +67,9 @@ def bomb_damage(self, bomb_sprite): self.image = modified_shield_surface # update the sprite mask so future collisions # use the mask rather than a basic rect - self.mask = pygame.mask.from_surface(modified_shield_surface) + self.mask = pygame.mask.from_surface( + self.modify_pixel_colors(modified_shield_surface) + ) def invader_damage(self, invader_sprite): shield_rect = self.rect diff --git a/classes/player/Player.py b/classes/player/Player.py deleted file mode 100644 index 21da71b..0000000 --- a/classes/player/Player.py +++ /dev/null @@ -1,20 +0,0 @@ -import pygame.sprite - - -class Player(pygame.sprite.Sprite): - def __init__(self): - super().__init__() - - sprite_path = "sprites/player/player-base.png" - self.image = pygame.image.load(sprite_path).convert_alpha() - self.rect = self.image.get_rect() - self.rect.x = 10 - self.rect.y = 218 - self.target_pos = self.rect.center - - # used by BombController - def get_rect(self): - return self.rect - - def draw(self, surface): - surface.blit(self.image, self.rect) diff --git a/classes/player/Player_controller.py b/classes/player/Player_controller.py deleted file mode 100644 index 95dcd23..0000000 --- a/classes/player/Player_controller.py +++ /dev/null @@ -1,44 +0,0 @@ -from classes.Controller import Controller -from classes.player.Player import Player - -player_speed = 1 - - -class PlayerController(Controller): - def __init__(self, config): - super().__init__(config) - self.can_launch_missile = True - self.enabled = False - self.player = Player() - - self.left_key_pressed = False - self.right_key_pressed = False - - def on_play_delay_complete(self, data): - self.enabled = True - - def on_move_left_exit(self, data): - self.left_key_pressed = False - - def on_move_left(self, data): - self.left_key_pressed = True - - def on_move_right_exit(self, data): - self.right_key_pressed = False - - def on_move_right(self, data): - self.right_key_pressed = True - - def update(self, events, dt): - if self.enabled: - if self.left_key_pressed: - self.player.rect.x -= player_speed * dt - elif self.right_key_pressed: - self.player.rect.x += player_speed * dt - return self.player - - def clamp(value, min_value, max_value): - return max(min(value, max_value), min_value) - - def get_player(self): - return self.player diff --git a/classes/player/Player_missile.py b/classes/player/Player_missile.py deleted file mode 100644 index 2a54ff5..0000000 --- a/classes/player/Player_missile.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import pygame.sprite - - -class PlayerMissile(pygame.sprite.Sprite): - def __init__(self, player_rect): - super().__init__() - - self.delay = 1 - sprite_path = "sprites/player/player-shot.png" - self.image = pygame.image.load(sprite_path).convert_alpha() - self.countdown = 0 - self.active = True - self.rect = self.image.get_rect() - self.rect.x = player_rect.x + 8 - self.rect.y = player_rect.y - self.explode_frame = pygame.image.load( - os.path.join("sprites", "player", "player-shot-explodes.png") - ) - - def modify_pixel_colors(self): - green = (0, 255, 0) - white = (255, 255, 255) - red = (255, 0, 0) - - for y in range(self.image.get_height()): - for x in range(self.image.get_width()): - pixel_color = self.image.get_at((x, y)) - if y + self.rect.y >= 191: - pixel_color.r, pixel_color.g, pixel_color.b = white - elif y + self.rect.y <= 35: - pixel_color.r, pixel_color.g, pixel_color.b = red - else: - pixel_color.r, pixel_color.g, pixel_color.b = green - - self.image.set_at((x, y), pixel_color) - - def draw(self, surface): - self.modify_pixel_colors() - surface.blit(self.image, self.rect) - - def remove(self): - self.kill() - - def explode(self, position_rect=None): - if self.active: - self.image = self.explode_frame - if position_rect: - print("updated rect") - self.rect.x = position_rect[0] - self.rect.y = position_rect[1] - - print(self.rect) - # if position_rect - # self.rect.x -= 4 - # self.rect.y -= 3 - self.active = False - self.countdown = 30 - - def update(self): - if self.countdown > 0: - self.countdown -= 1 - if self.countdown <= 0: - self.kill() - else: - # if self.delay <= 0: - self.delay = 1 - self.rect.y -= 4 # Move the missile vertically upwards - if self.rect.y <= 42: - self.explode(()) - # else: - # self.delay -= 1 - return self diff --git a/classes/player/Player_missile_controller.py b/classes/player/Player_missile_controller.py deleted file mode 100644 index 8646b6d..0000000 --- a/classes/player/Player_missile_controller.py +++ /dev/null @@ -1,82 +0,0 @@ -from classes.Controller import Controller -from classes.player.Player_missile import PlayerMissile -import pygame - - -class PlayerMissileController(Controller): - def __init__(self): - super().__init__([]) - self.ready_flag = False - # there will only ever be one sprite in this group - self.missile_group = pygame.sprite.Group() - - # callbacks for interacting with other components - self.get_invaders_callback = lambda: None - self.get_shields_callback = lambda: None - self.get_player_callback = lambda: None - self.mothership_is_exploding = lambda: None - - def on_missile_ready(self, data): - self.ready_flag = True - - def on_fire_pressed(self, data): - if ( - not self.missile_group - and self.ready_flag - and not self.mothership_is_exploding() - ): - self.missile_group.add(PlayerMissile(self.get_player_callback().rect)) - - def check_collisions(self): - if self.missile_group: - collided_invaders = pygame.sprite.spritecollide( - self.missile_group.sprites()[0], self.get_invaders_callback(), False - ) - - if collided_invaders: - self.event_manager.notify("invader_hit", collided_invaders[0]) - self.ready_flag = False - return True - - def update(self, events, dt): - if self.missile_group: - if not self.check_collisions(): - return self.missile_group.sprites()[0].update() - else: - self.missile_group.remove(self.missile_group.sprites()[0]) - - # shields need access to player missile - def get_player_missile(self): - if self.missile_group: - return self.missile_group.sprites()[0] - - # if self.player_missile: - # return self.player_missile.update() - # else: - # print("nothing..") - - # if not self.countdown > 0: - # if self.player_missile: - # self.check_collisions() - # self.player_missile.rect.y -= 5 - # if self.player_missile.rect.y <= 0: - # self.player_missile.explode() - # self.countdown = 15 - # else: - # print(self.countdown) - # self.countdown -= 1 - # if self.countdown <= 0: - # self.player_missile = None - # self.ready_flag = True - - # return self.player_missile - - # def explode(self): - # self.countdown -= 1 - # if self.countdown <= 0: - # self.player_missile = None - # self.ready_flag = True - - # def destroy_player_missile(self): - # self.player_missile = None - # self.ready_flag = True diff --git a/game.py b/game.py index e15b6bb..f940958 100644 --- a/game.py +++ b/game.py @@ -1,27 +1,38 @@ import pygame, time from pygame.locals import * -from classes.Game_controller import GameController -from classes.Config import Config +from states.State_intro import StateIntro +from states.State_game_starts import StateGameStarts +from states.State_game_playing import StateGamePlaying +from states.State_player_exploding import StatePlayerExploding +from classes.State_machine import StateMachine -config = Config() pygame.init() -last_time = time.time() +states = { + "GAME_INTRO": StateIntro(), + "GAME_START": StateGameStarts(), + "GAME_PLAYING": StateGamePlaying(), + "PLAYER_EXPLODING": StatePlayerExploding(), +} -game_controller = GameController(config) -running = True +# system = System() +state_machine = StateMachine(states, "GAME_INTRO") +running = True +max_fps = 60 +clock = pygame.time.Clock() while running: - dt = time.time() - last_time - dt *= 60 - last_time = time.time() - events = [] for event in pygame.event.get(): if event.type == QUIT: running = False else: events.append(event) - game_controller.update(events, dt) + + state_machine.update(events) + + pygame.display.flip() + clock.tick(max_fps) + pygame.quit() diff --git a/images/font_spritesheet.png b/images/font_spritesheet.png deleted file mode 100644 index c600e9f..0000000 Binary files a/images/font_spritesheet.png and /dev/null differ diff --git a/images/sprite_sheet.png b/images/sprite_sheet.png new file mode 100644 index 0000000..e3727c9 Binary files /dev/null and b/images/sprite_sheet.png differ diff --git a/lib/Controller.py b/lib/Controller.py new file mode 100644 index 0000000..caa9119 --- /dev/null +++ b/lib/Controller.py @@ -0,0 +1,47 @@ +from lib.Event_manager import EventManager + + +class Controller: + def __init__(self): + self.event_manager = EventManager.get_instance() + + # Class-level dictionary to store callbacks + callbacks = {} + + # Class-level dictionary to store callback names (labels) + callback_names = {} + + @classmethod + def register_callback(cls, key, callback, name=None): + cls.callbacks[key] = callback + + # Store the callback name if provided + if name: + cls.callback_names[key] = name + + @classmethod + def get_callback(cls, key): + return cls.callbacks.get(key, None) + + @classmethod + def callback(cls, key): + callback_function = cls.callbacks.get(key) + if callback_function is not None: + return callback_function() + else: + # Optionally handle the case where the key is not found + # print(f"No callback found for key: {key}") + return None + + @classmethod + def debug_callbacks(cls): + print("Callbacks:") + for key, callback in cls.callbacks.items(): + # Get the callback name from the dictionary, or use the key as a fallback + name = cls.callback_names.get(key, key) + print( + f"{name}: {callback.__name__ if hasattr(callback, '__name__') else callback}" + ) + + +# pygame.quit() diff --git a/classes/Event_manager.py b/lib/Event_manager.py similarity index 100% rename from classes/Event_manager.py rename to lib/Event_manager.py diff --git a/lib/Game_sprite.py b/lib/Game_sprite.py new file mode 100644 index 0000000..531fd3e --- /dev/null +++ b/lib/Game_sprite.py @@ -0,0 +1,54 @@ +import pygame + + +class GameSprite(pygame.sprite.Sprite): + def __init__(self): + super().__init__() + + def modify_pixel_colors(self, image): + if isinstance(image, pygame.Surface): + return self.apply_image_tints(image) + else: + print("not an image being converted") + + def apply_image_tints(self, image): + area_colors = [0, 1, 0, 3, 0, 3, 0] # Corresponding colors for each area + areas = [ + {"position": (0, 0), "size": (224, 32)}, + {"position": (0, 32), "size": (224, 32)}, + {"position": (0, 64), "size": (224, 120)}, + {"position": (0, 184), "size": (224, 56)}, + {"position": (0, 240), "size": (24, 16)}, + {"position": (24, 240), "size": (112, 16)}, + {"position": (136, 240), "size": (88, 16)}, + ] + + # banding colours + colors = [ + (255, 255, 255), # White + (255, 0, 0), # Red + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + ] + + for y in range(image.get_height()): + for x in range(image.get_width()): + pixel_color = image.get_at((x, y)) + pixel_x, pixel_y = ( + self.rect.x + x, + self.rect.y + y, + ) # Pixel position in the sprite's coordinate system + + for i, area in enumerate(areas): + area_rect = pygame.Rect(area["position"], area["size"]) + if area_rect.collidepoint(pixel_x, pixel_y): + color_index = area_colors[i] + new_color = colors[color_index] + pixel_color.r, pixel_color.g, pixel_color.b = new_color + + image.set_at((x, y), pixel_color) + + return image diff --git a/lib/Image_tint.py b/lib/Image_tint.py new file mode 100644 index 0000000..8586169 --- /dev/null +++ b/lib/Image_tint.py @@ -0,0 +1,54 @@ +import pygame + + +class Image_tint: + def modify_pixel_colors(self, image): + if isinstance(image, pygame.Surface): + return self.apply_image_tints(image) + else: + print("not an image being converted") + + def apply_image_tints(self, image): + area_colors = [0, 1, 0, 3, 0, 3, 0] # Corresponding colors for each area + areas = [ + {"position": (0, 0), "size": (224, 32)}, + {"position": (0, 32), "size": (224, 32)}, + {"position": (0, 64), "size": (224, 120)}, + {"position": (0, 184), "size": (224, 56)}, + {"position": (0, 240), "size": (24, 16)}, + {"position": (24, 240), "size": (112, 16)}, + {"position": (136, 240), "size": (88, 16)}, + ] + + # banding colours + colors = [ + (255, 255, 255), # White + (255, 0, 0), # Red + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + (0, 255, 0), # Green + (255, 255, 255), # White + ] + + for y in range(image.get_height()): + for x in range(image.get_width()): + pixel_color = image.get_at((x, y)) + pixel_x, pixel_y = ( + self.rect.x + x, + self.rect.y + y, + ) # Pixel position in the sprite's coordinate system + + for i, area in enumerate(areas): + area_rect = pygame.Rect(area["position"], area["size"]) + if area_rect.collidepoint(pixel_x, pixel_y): + color_index = area_colors[i] + new_color = colors[color_index] + pixel_color.r, pixel_color.g, pixel_color.b = new_color + + image.set_at((x, y), pixel_color) + + return image + + +__all__ = ["modify_pixel_colors"] diff --git a/lib/Sprite_sheet.py b/lib/Sprite_sheet.py new file mode 100644 index 0000000..7ffd9ae --- /dev/null +++ b/lib/Sprite_sheet.py @@ -0,0 +1,140 @@ +import pygame + + +class SpriteSheet: + def __init__(self): + self.sprite_sheet = pygame.image.load("images/sprite_sheet.png").convert_alpha() + self.sprite_lookup = {} + + def add_sprite(self, name, x, y, width, height): + self.sprite_lookup[name] = (x, y, width, height) + + def get_sprite(self, name): + sprite_rect = self.sprite_lookup.get(name) + if sprite_rect: + x, y, width, height = sprite_rect + sprite = pygame.Surface((width, height), pygame.SRCALPHA) + sprite.blit(self.sprite_sheet, (0, 0), (x, y, width, height)) + return sprite + else: + raise ValueError(f"Sprite with name '{name}' not found in the spritesheet.") + + +class InvaderSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("invader_small_frame1", 1, 1, 16, 8) + self.add_sprite("invader_small_frame2", 1, 11, 16, 8) + + self.add_sprite("invader_mid_frame1", 19, 1, 16, 8) + self.add_sprite("invader_mid_frame2", 19, 11, 16, 8) + + self.add_sprite("invader_large_frame1", 37, 1, 16, 8) + self.add_sprite("invader_large_frame2", 37, 11, 16, 8) + + self.add_sprite("invader_explode_frame", 55, 1, 16, 8) + + +class BombSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("plunger_frame1", 21, 21, 3, 8) + self.add_sprite("plunger_frame2", 26, 21, 3, 8) + self.add_sprite("plunger_frame3", 31, 21, 3, 8) + self.add_sprite("plunger_frame4", 36, 21, 3, 8) + + self.add_sprite("squiggly_frame1", 1, 21, 3, 8) + self.add_sprite("squiggly_frame2", 6, 21, 3, 8) + self.add_sprite("squiggly_frame3", 11, 21, 3, 8) + self.add_sprite("squiggly_frame4", 16, 21, 3, 8) + + self.add_sprite("rolling_frame1", 41, 21, 3, 8) + self.add_sprite("rolling_frame2", 46, 21, 3, 8) + self.add_sprite("rolling_frame3", 51, 21, 3, 8) + self.add_sprite("rolling_frame4", 56, 21, 3, 8) + + self.add_sprite("explode_frame", 61, 21, 6, 8) + + +class ShieldSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("shield_frame", 45, 31, 24, 16) + + +class MothershipSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("mothership_frame", 1, 39, 16, 8) + self.add_sprite("explode_frame", 19, 39, 24, 8) + + +class PlayerSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + self.add_sprite("player", 1, 49, 16, 8) + self.add_sprite("player_explode1", 19, 49, 16, 8) + self.add_sprite("player_explode2", 37, 49, 16, 8) + self.add_sprite("missile", 55, 49, 1, 8) + + self.add_sprite("missile_2x", 68, 49, 1, 8) + self.add_sprite("missile_explode", 58, 49, 8, 8) + + +class FontSpriteSheet(SpriteSheet): + def __init__(self): + super().__init__() + + self.add_sprite("A", 1, 69, 8, 8) + self.add_sprite("B", 11, 69, 8, 8) + self.add_sprite("C", 21, 69, 8, 8) + self.add_sprite("D", 31, 69, 8, 8) + self.add_sprite("E", 41, 69, 8, 8) + self.add_sprite("F", 51, 69, 8, 8) + self.add_sprite("G", 61, 69, 8, 8) + self.add_sprite("H", 71, 69, 8, 8) + + self.add_sprite("I", 1, 79, 8, 8) + self.add_sprite("J", 11, 79, 8, 8) + self.add_sprite("K", 21, 79, 8, 8) + self.add_sprite("L", 31, 79, 8, 8) + self.add_sprite("M", 41, 79, 8, 8) + self.add_sprite("N", 51, 79, 8, 8) + self.add_sprite("O", 61, 79, 8, 8) + self.add_sprite("P", 71, 79, 8, 8) + + self.add_sprite("Q", 1, 89, 8, 8) + self.add_sprite("R", 11, 89, 8, 8) + self.add_sprite("S", 21, 89, 8, 8) + self.add_sprite("T", 31, 89, 8, 8) + self.add_sprite("U", 41, 89, 8, 8) + self.add_sprite("V", 51, 89, 8, 8) + self.add_sprite("W", 61, 89, 8, 8) + self.add_sprite("X", 71, 89, 8, 8) + + self.add_sprite("Y", 1, 99, 8, 8) + self.add_sprite("Z", 11, 99, 8, 8) + self.add_sprite("0", 21, 99, 8, 8) + self.add_sprite("1", 31, 99, 8, 8) + self.add_sprite("2", 41, 99, 8, 8) + self.add_sprite("3", 51, 99, 8, 8) + self.add_sprite("4", 61, 99, 8, 8) + self.add_sprite("5", 71, 99, 8, 8) + + self.add_sprite("6", 1, 109, 8, 8) + self.add_sprite("7", 11, 109, 8, 8) + self.add_sprite("8", 21, 109, 8, 8) + self.add_sprite("9", 31, 109, 8, 8) + + self.add_sprite("<", 41, 109, 8, 8) + self.add_sprite(">", 51, 109, 8, 8) + self.add_sprite("=", 61, 109, 8, 8) + self.add_sprite("*", 71, 109, 8, 8) + + self.add_sprite("?", 1, 119, 8, 8) + self.add_sprite("-", 11, 119, 8, 8) + self.add_sprite("%", 1, 59, 8, 8) # index for upside down Y diff --git a/space_invaders.ttf b/space_invaders.ttf deleted file mode 100644 index c613ec7..0000000 Binary files a/space_invaders.ttf and /dev/null differ diff --git a/sprites/invader/invader-empty-frame.png b/sprites/invader/invader-empty-frame.png deleted file mode 100644 index f6268bc..0000000 Binary files a/sprites/invader/invader-empty-frame.png and /dev/null differ diff --git a/sprites/invader/invader-explode.png b/sprites/invader/invader-explode.png deleted file mode 100644 index 219e4a8..0000000 Binary files a/sprites/invader/invader-explode.png and /dev/null differ diff --git a/sprites/invader/invader-large-frame1.png b/sprites/invader/invader-large-frame1.png deleted file mode 100644 index 2248537..0000000 Binary files a/sprites/invader/invader-large-frame1.png and /dev/null differ diff --git a/sprites/invader/invader-large-frame2.png b/sprites/invader/invader-large-frame2.png deleted file mode 100644 index d45a818..0000000 Binary files a/sprites/invader/invader-large-frame2.png and /dev/null differ diff --git a/sprites/invader/invader-mid-frame1.png b/sprites/invader/invader-mid-frame1.png deleted file mode 100644 index 2cfb52c..0000000 Binary files a/sprites/invader/invader-mid-frame1.png and /dev/null differ diff --git a/sprites/invader/invader-mid-frame2.png b/sprites/invader/invader-mid-frame2.png deleted file mode 100644 index 2649ef6..0000000 Binary files a/sprites/invader/invader-mid-frame2.png and /dev/null differ diff --git a/sprites/invader/invader-small-frame1.png b/sprites/invader/invader-small-frame1.png deleted file mode 100644 index 31f8403..0000000 Binary files a/sprites/invader/invader-small-frame1.png and /dev/null differ diff --git a/sprites/invader/invader-small-frame2.png b/sprites/invader/invader-small-frame2.png deleted file mode 100644 index 4adda3c..0000000 Binary files a/sprites/invader/invader-small-frame2.png and /dev/null differ diff --git a/sprites/invader_bomb/bomb_exploding.png b/sprites/invader_bomb/bomb_exploding.png deleted file mode 100644 index a19be11..0000000 Binary files a/sprites/invader_bomb/bomb_exploding.png and /dev/null differ diff --git a/sprites/invader_bomb/bomb_exploding_base.png b/sprites/invader_bomb/bomb_exploding_base.png deleted file mode 100644 index 097f58d..0000000 Binary files a/sprites/invader_bomb/bomb_exploding_base.png and /dev/null differ diff --git a/sprites/invader_bomb/explode-stem.png b/sprites/invader_bomb/explode-stem.png deleted file mode 100644 index c499666..0000000 Binary files a/sprites/invader_bomb/explode-stem.png and /dev/null differ diff --git a/sprites/invader_bomb/plunger-frame1.png b/sprites/invader_bomb/plunger-frame1.png deleted file mode 100644 index e7aef2b..0000000 Binary files a/sprites/invader_bomb/plunger-frame1.png and /dev/null differ diff --git a/sprites/invader_bomb/plunger-frame2.png b/sprites/invader_bomb/plunger-frame2.png deleted file mode 100644 index da1491d..0000000 Binary files a/sprites/invader_bomb/plunger-frame2.png and /dev/null differ diff --git a/sprites/invader_bomb/plunger-frame3.png b/sprites/invader_bomb/plunger-frame3.png deleted file mode 100644 index 0b398a3..0000000 Binary files a/sprites/invader_bomb/plunger-frame3.png and /dev/null differ diff --git a/sprites/invader_bomb/plunger-frame4.png b/sprites/invader_bomb/plunger-frame4.png deleted file mode 100644 index d504361..0000000 Binary files a/sprites/invader_bomb/plunger-frame4.png and /dev/null differ diff --git a/sprites/invader_bomb/rolling-frame1.png b/sprites/invader_bomb/rolling-frame1.png deleted file mode 100644 index a8876c3..0000000 Binary files a/sprites/invader_bomb/rolling-frame1.png and /dev/null differ diff --git a/sprites/invader_bomb/rolling-frame2.png b/sprites/invader_bomb/rolling-frame2.png deleted file mode 100644 index 6fdde2e..0000000 Binary files a/sprites/invader_bomb/rolling-frame2.png and /dev/null differ diff --git a/sprites/invader_bomb/rolling-frame3.png b/sprites/invader_bomb/rolling-frame3.png deleted file mode 100644 index a8876c3..0000000 Binary files a/sprites/invader_bomb/rolling-frame3.png and /dev/null differ diff --git a/sprites/invader_bomb/rolling-frame4.png b/sprites/invader_bomb/rolling-frame4.png deleted file mode 100644 index d8dbdbd..0000000 Binary files a/sprites/invader_bomb/rolling-frame4.png and /dev/null differ diff --git a/sprites/invader_bomb/squiggly-frame1.png b/sprites/invader_bomb/squiggly-frame1.png deleted file mode 100644 index 53b6259..0000000 Binary files a/sprites/invader_bomb/squiggly-frame1.png and /dev/null differ diff --git a/sprites/invader_bomb/squiggly-frame2.png b/sprites/invader_bomb/squiggly-frame2.png deleted file mode 100644 index 5513f36..0000000 Binary files a/sprites/invader_bomb/squiggly-frame2.png and /dev/null differ diff --git a/sprites/invader_bomb/squiggly-frame3.png b/sprites/invader_bomb/squiggly-frame3.png deleted file mode 100644 index 4b2018c..0000000 Binary files a/sprites/invader_bomb/squiggly-frame3.png and /dev/null differ diff --git a/sprites/invader_bomb/squiggly-frame4.png b/sprites/invader_bomb/squiggly-frame4.png deleted file mode 100644 index d28019e..0000000 Binary files a/sprites/invader_bomb/squiggly-frame4.png and /dev/null differ diff --git a/sprites/mothership/mothership-exploding.png b/sprites/mothership/mothership-exploding.png deleted file mode 100644 index 4a242a0..0000000 Binary files a/sprites/mothership/mothership-exploding.png and /dev/null differ diff --git a/sprites/mothership/mothership.png b/sprites/mothership/mothership.png deleted file mode 100644 index 008bab8..0000000 Binary files a/sprites/mothership/mothership.png and /dev/null differ diff --git a/sprites/player/player-base-explodes-frame1.png b/sprites/player/player-base-explodes-frame1.png deleted file mode 100644 index af92f76..0000000 Binary files a/sprites/player/player-base-explodes-frame1.png and /dev/null differ diff --git a/sprites/player/player-base-explodes-frame2.png b/sprites/player/player-base-explodes-frame2.png deleted file mode 100644 index 53bb93f..0000000 Binary files a/sprites/player/player-base-explodes-frame2.png and /dev/null differ diff --git a/sprites/player/player-base.png b/sprites/player/player-base.png deleted file mode 100644 index aa77f37..0000000 Binary files a/sprites/player/player-base.png and /dev/null differ diff --git a/sprites/player/player-shield.png b/sprites/player/player-shield.png deleted file mode 100644 index eb80bc0..0000000 Binary files a/sprites/player/player-shield.png and /dev/null differ diff --git a/sprites/player/player-shot-double-height.png b/sprites/player/player-shot-double-height.png deleted file mode 100644 index 93a8baa..0000000 Binary files a/sprites/player/player-shot-double-height.png and /dev/null differ diff --git a/sprites/player/player-shot-explodes.png b/sprites/player/player-shot-explodes.png deleted file mode 100644 index ce7b2fb..0000000 Binary files a/sprites/player/player-shot-explodes.png and /dev/null differ diff --git a/sprites/player/player-shot.png b/sprites/player/player-shot.png deleted file mode 100644 index 6ad61c7..0000000 Binary files a/sprites/player/player-shot.png and /dev/null differ diff --git a/states/State_game_playing.py b/states/State_game_playing.py new file mode 100644 index 0000000..589ed02 --- /dev/null +++ b/states/State_game_playing.py @@ -0,0 +1,28 @@ +class StateGamePlaying: + def __init__(self): + print("initialised state playing") + # self.state_machine = state_machine + ## maybe load in specific controllers here? + + ## how to get controllers to indicate when the state has changed + ## use callbacks or events? + ## pass the statemachine/state to each controller? + ## have the game controller receive the statemachine? + + ## give the invader_controller a hook into this code + ## have invader_controller call a function in this state when an event happens + + # load controllers needed + # add events betwen the controller back to the state functions + # invader_controller.event_manager.add_listener("invader_hit", self.on_invader_hit) + + def enter(self): + # code that should run when entering this state + print("entered state game playing") + + def update(self): + # code that should run every frame + print("in update on game playing") + + def exit(self, next_state): + self.state_machine.change_to(next_state) diff --git a/states/State_game_starts.py b/states/State_game_starts.py new file mode 100644 index 0000000..f54e37e --- /dev/null +++ b/states/State_game_starts.py @@ -0,0 +1,148 @@ +from classes.System import System + +# hold all game state here for all controllers to use +# allow controllers to access state related to other controllers? +# create a basic state class which has common functions +class StateGameStarts: + def __init__(self): + self.system = System.get_instance() + + # we'll pass the state array to all controllers so they can share the state + # and we'll allow controllers to update the state back up + self.state = { + 'invaders_moving': True, + 'is_moving_left': False, + 'is_moving_right': False, + 'fire_button_pressed': False, + 'player_enabled': True, + 'bombs_enabled': True, + } + + # self.state = { + # 'invaders_moving': False, + # 'is_moving_left': False, + # 'is_moving_right': False, + # + # 'fire_button_pressed': { + # 'default': False,#needed if reseting use default + # 'one_shot': True, + # 'value': False + # } + # } + + print("initialised state game") + + def reset_state(self, index): + pass + # this would return the state identified by index back to its reset value + + def update_state(returned_state): + self.state = returned_state + + # update to be able to specify 'one shot' = True + # could reset on subsequent passes + ### maybe even store previous value #### + def set_state(self, index, value): + self.state[index] = value + + def get_state_value(self, index): + if index in self.state: + return self.state[index] + + + def enter(self, state_machine): + self.state_machine = state_machine + + self.invader_controller = self.system.get_controller("Invader") + self.input_controller = self.system.get_controller("Input") + self.shield_controller = self.system.get_controller("Shield") + self.bomb_controller = self.system.get_controller("Bomb") + self.player_controller = self.system.get_controller("Player") + self.player_missile_controller = self.system.get_controller("PlayerMissile") + + + self.bomb_controller.enabled = True + self.player_controller.spawn_player() + self.player_missile_controller.ready_flag = True + + #self.system.add_listener("player_explodes", self.on_player_hit) + self.system.add_listener("invader_hit", self.invader_controller.on_invader_hit) + + + #player events + self.system.add_listener("left_button_pressed", self.on_move_left) + self.system.add_listener("left_button_released", self.on_move_left_exit) + self.system.add_listener("right_button_pressed", self.on_move_right) + self.system.add_listener("right_button_released", self.on_move_right_exit) + self.system.add_listener("fire_button_pressed", self.on_fire_pressed) + + #self.system.register_callback("get_player", self.player_controller.get_player) + + self.player_missile_controller.get_player_callback = self.player_controller.get_player + self.player_missile_controller.get_invaders_callback = self.invader_controller.get_invaders + + self.bomb_controller.get_invaders_callback = self.invader_controller.get_invaders + self.bomb_controller.get_player_callback = self.player_controller.get_player + self.bomb_controller.get_invaders_clearpath_callback = self.invader_controller.invader_container.get_invaders_with_clear_path + + self.shield_controller.get_invaders_callback = self.invader_controller.get_invaders + self.shield_controller.get_bombs_callback = self.bomb_controller.get_bombs + + self.shield_controller.get_player_missile_callback = self.player_missile_controller.get_player_missile + + print(self.shield_controller.get_player_missile_callback) + + + #self.system.register_callback("get_invaders", self.invader_controller.get_invaders) + #self.system.register_callback("get_invaders_with_clear_path", lambda: self.invader_controller.invader_container.get_invaders_with_clear_path()) + #self.system.register_callback("get_lowest_invader_y", lambda: self.invader_controller.invader_container.get_invaders()[0].rect.y) + #self.system.register_callback("get_invader_count", lambda: len(self.invader_controller.invader_container.get_invaders())) + + + + # have a function that sets state on a index of the dict and specify whether it clears or changes on subsequent updates + + def on_fire_pressed(self, data): + self.set_state('fire_button_pressed', True) + + def on_move_left(self, data): + self.set_state('is_moving_left', True) + + def on_move_left_exit(self, data): + self.set_state('is_moving_left', False) + + def on_move_right(self, data): + self.set_state('is_moving_right', True) + + def on_move_right_exit(self, data): + self.set_state('is_moving_right', False) + + def on_fire_button_pressed(self, data): + print("fire button caught in state game starts") + self.exit("GAME_INTRO") + + def on_player_hit(self, data): + print("player was hit") + #self.exit("PLAYER_EXPLODING") + + def update(self, events): + self.input_controller.update(events, self.state) + self.invader_controller.update(events, self.state) + self.shield_controller.update(events, self.state) + self.bomb_controller.update(events, self.state) + self.player_controller.update(events, self.state) + self.player_missile_controller.update(events, self.state) + self.set_state('fire_button_pressed', False) + + def get_surfaces(self): + return [ + self.player_missile_controller.get_surface(), + self.invader_controller.get_surface(), + self.shield_controller.get_surface(), + self.bomb_controller.get_surface(), + self.player_controller.get_surface(), + + ] + + def exit(self, next_state): + self.state_machine.change_to(next_state) diff --git a/states/State_intro.py b/states/State_intro.py new file mode 100644 index 0000000..d1c5958 --- /dev/null +++ b/states/State_intro.py @@ -0,0 +1,52 @@ +from classes.System import System + + +class StateIntro: + def __init__(self): + self.system = System.get_instance() + # at this point there is no concept of state machine + print("initialised state intro") + self.state = {} + + # inject functions rather than whole statemachine + # maybe inject + def enter(self, state_machine): + self.state['invaders_moving'] = True + self.state_machine = state_machine + self.invader_controller = self.system.get_controller("Invader") + self.input_controller = self.system.get_controller("Input") + self.shield_controller = self.system.get_controller("Shield") + + # code that should run when entering this state + self.system.add_listener("escape_button_pressed", self.on_escape_button_pressed) + # self.system.add_listener("invader_hit", on_invader_hit) + # def on_invader_hit() self.invader_controller.... + + self.system.register_callback("get_invaders", self.invader_controller.get_invaders) + + self.system.debug_callbacks() + get_invaders_callback = self.system.get_callback("get_invaders") + #print(get_invaders_callback) + + print("entered state intro state") + + def update(self, events): + self.input_controller.update(events, self.state) + self.invader_controller.update(events, self.state), + self.shield_controller.update(events, self.state), + + def get_surfaces(self): + return [ + self.invader_controller.get_surface(), + self.shield_controller.get_surface(), + ] + + def on_escape_button_pressed(self, data): + print("escape caught in state intro") + self.exit("GAME_START") + + def exit(self, next_state): + self.system.remove_listener( + "escape_button_pressed", self.on_escape_button_pressed + ) + self.state_machine.change_to(next_state) diff --git a/states/State_player_exploding.py b/states/State_player_exploding.py new file mode 100644 index 0000000..db6fd03 --- /dev/null +++ b/states/State_player_exploding.py @@ -0,0 +1,60 @@ +from classes.System import System + + +class StatePlayerExploding: + def __init__(self): + self.system = System.get_instance() + + self.state = { + 'invaders_moving': False, + } + + print("initialised state StatePlayerExploding") + # self.event_manager.add_listener( + # "player_explosion_complete", self.on_player_explosion_complete + # ) + + def set_state(self, index, value): + self.state[index] = value + + def get_state_value(self, index): + if index in self.state: + return self.state[index] + + def enter(self, state_machine): + print("entered player explode state") + self.state_machine = state_machine + + self.invader_controller = self.system.get_controller("Invader") + self.shield_controller = self.system.get_controller("Shield") + self.bomb_controller = self.system.get_controller("Bomb") + self.player_controller = self.system.get_controller("Player") + self.player_missile_controller = self.system.get_controller("PlayerMissile") + + self.bomb_controller.enabled = False + self.player_controller.explode_player() + #self.player_missile_controller.get_player_callback = self.player_controller.get_player + + def on_player_explosion_complete(self, data): + print("in state explode - animation complete") + + def update(self, events): + self.invader_controller.update(events, self.state) + self.shield_controller.update(events, self.state) + self.bomb_controller.update(events, self.state) + self.player_controller.update(events, self.state) + self.player_missile_controller.update(events, self.state) + + # use array and look for method on controller get_surface + def get_surfaces(self): + return [ + self.player_missile_controller.get_surface(), + self.invader_controller.get_surface(), + self.shield_controller.get_surface(), + self.bomb_controller.get_surface(), + self.player_controller.get_surface(), + ] + + + def exit(self, next_state): + self.state_machine.change_to(next_state) diff --git a/text.py b/text.py new file mode 100644 index 0000000..c8b8207 --- /dev/null +++ b/text.py @@ -0,0 +1,56 @@ +import pygame +import sys +import time + + +class GameOverAnimation: + def __init__(self): + pygame.init() + self.SCREEN_WIDTH, self.SCREEN_HEIGHT = 800, 600 + self.BACKGROUND_COLOR = (255, 255, 255) + self.TEXT_COLOR = (0, 0, 0) + self.FONT_SIZE = 36 + self.FONT_NAME = "Arial" + self.game_over_text = "GAME OVER" + self.font = pygame.font.Font(None, self.FONT_SIZE) + self.screen = pygame.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT)) + pygame.display.set_caption("Game Over Animation") + + def create_text_surface(self, text): + return self.font.render(text, True, self.TEXT_COLOR) + + def run(self): + running = True + current_text = "" + char_timer = 0 + char_index = 0 + + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + self.screen.fill(self.BACKGROUND_COLOR) + + if char_index < len(self.game_over_text): + current_text += self.game_over_text[char_index] + char_index += 1 + + text_surface = self.create_text_surface(current_text) + + text_rect = text_surface.get_rect() + text_rect.center = (self.SCREEN_WIDTH // 2, self.SCREEN_HEIGHT // 2) + + self.screen.blit(text_surface, text_rect) + pygame.display.flip() + + if char_index < len(self.game_over_text): + time.sleep(1) # Wait 1 second for each character + + pygame.quit() + sys.exit() + + +if __name__ == "__main__": + game = GameOverAnimation() + game.run()