diff --git a/MARWIN_ADVENTURE.py b/MARWIN_ADVENTURE.py
new file mode 100644
index 0000000..aa94dd7
--- /dev/null
+++ b/MARWIN_ADVENTURE.py
@@ -0,0 +1,147 @@
+from button import Button
+import pygame
+import sys
+import settings as config
+pygame.init()
+
+# Screen state flags
+is_darkening = False # Start darkening animation
+is_finished_darkening = False # Darkening animation finished
+overlay_alpha = 0 # Darkness value
+
+overlay_surface = pygame.Surface((config.XWIN, config.YWIN)) # Create surface for dimming
+window = pygame.display.set_mode(config.DISPLAY, config.FLAGS) # Screen
+pygame.display.set_caption("MARWIN ADVENTURE") # Set title
+pygame.display.set_icon(pygame.image.load(config.icon)) # Set icon
+
+BG = pygame.image.load(config.main_menu_background) # Background
+
+# Button images
+play_button_image = pygame.image.load(config.start_button_non_active) # START
+exit_button_image = pygame.image.load(config.exit_button_non_active) # EXIT
+
+# Sound flags
+play_button_PLAY_sound = False # Play button sound activation
+play_button_EXIT_sound = False # Exit button sound activation
+
+# Darkening animation
+def darken_screen():
+ global overlay_alpha, is_darkening, is_finished_darkening # Global variables
+ overlay_alpha += 5 # Increase darkness value
+ if overlay_alpha >= 255:
+ # Animation completion
+ is_darkening = False
+ is_finished_darkening = True
+ overlay_surface.set_alpha(overlay_alpha) # Set darkness
+ if config.music_volume > 0.0:
+ config.music_volume -= 0.001
+ pygame.mixer.music.set_volume(config.music_volume) # Apply reduced volume
+ window.blit(overlay_surface, (0, 0)) # Draw
+
+# Button action functions
+def start_game(): # Start game for START button
+ global is_darkening
+ is_darkening = True
+ play_button.clicked = True
+ darken_screen()
+
+def exit_game(): # Exit game for EXIT button
+ pygame.quit()
+ sys.exit()
+
+# Text display
+def display_text(text, font_size, color, font_path, pos):
+ font = pygame.font.Font(font_path, font_size)
+ text_render = font.render(text, True, color)
+ text_rect = text_render.get_rect(center=pos)
+ window.blit(text_render, text_rect)
+
+# Button instances
+play_button = Button(play_button_image, (config.XWIN // 2, config.YWIN // 2), action=start_game)
+exit_button = Button(exit_button_image, (config.XWIN // 2, config.YWIN // 1.35), action=exit_game)
+
+def Run():
+ global overlay_alpha, is_darkening, is_finished_darkening, play_button_PLAY_sound, play_button_EXIT_sound
+ # Reset before loop start
+ config.music_volume = 0.2
+ is_finished_darkening = False
+ is_darkening = False
+ overlay_alpha = 0
+
+ # Load animation background
+ bg_y = 0 # Background coordinates
+ bg_speed = 1 # Background scroll speed
+
+ # Start music
+ pygame.mixer.music.load(config.overworld_music)
+ pygame.mixer.music.set_volume(config.music_volume)
+ pygame.mixer.music.play(-1)
+
+ while True:
+ mouse_pos = pygame.mouse.get_pos() # Handle mouse position
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ pygame.quit()
+ sys.exit()
+ elif event.type == pygame.MOUSEBUTTONDOWN:
+ # Button function activation on click
+ play_button.check_click(mouse_pos)
+ exit_button.check_click(mouse_pos)
+
+ # If darkening animation finished, launch main.py
+ if is_finished_darkening:
+ pygame.mixer.music.stop()
+ import main # Import
+ main.run_game() # Initialization
+ # Reset parameters after launch
+ is_finished_darkening = False
+ is_darkening = False
+ overlay_alpha = 0
+
+ # Display background and buttons
+ window.blit(BG, (0, bg_y))
+ window.blit(BG, (0, bg_y - config.YWIN)) # Extra display for covering image boundary
+
+ # Update background position for vertical scrolling
+ bg_y += bg_speed
+ if bg_y >= config.YWIN:
+ bg_y = 0
+
+ # Display buttons and handle their hover reactions
+ if not is_darkening or not is_finished_darkening: # If not in darkening animation
+ # Play button
+ if play_button.rect.collidepoint(mouse_pos):
+ if play_button_PLAY_sound:
+ config.check_button.play()
+ play_button_PLAY_sound = False
+ window.blit(pygame.image.load(config.start_button_active), play_button.rect)
+ else:
+ play_button_PLAY_sound = True
+ window.blit(play_button.image, play_button.rect)
+
+ # Exit button
+ if exit_button.rect.collidepoint(mouse_pos):
+ if play_button_EXIT_sound:
+ config.check_button.play()
+ play_button_EXIT_sound = False
+ window.blit(pygame.image.load(config.exit_button_active), exit_button.rect)
+ else:
+ play_button_EXIT_sound = True
+ window.blit(exit_button.image, exit_button.rect)
+
+ # Display MARWIN image
+ display_text("MARWIN", 80, config.DARK_BLUE, config.pixel_font, (config.XWIN // 2, config.YWIN // 6))
+ display_text("ADVENTURE", 80, config.DARK_BLUE, config.pixel_font, (config.XWIN // 2, config.YWIN // 3.5))
+
+ if is_darkening: # Screen darkening at game start
+ darken_screen()
+
+ pygame.display.update() # Update screen
+ pygame.time.Clock().tick(config.FPS) # Set FPS
+
+def main(): # Function to initialize launch, allowing to return to the main menu while in the game
+ Run()
+
+if __name__ == "__main__":
+ # Program start
+ main()
diff --git a/README.md b/README.md
index 57c610a..c142c90 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,17 @@
-# Pygame DoodleJump
-The simplest doodle jump made with python3 and pygame in only 2 days
+# Pygame DoodleJump (MARWIN ADVENTURE)
+### The game like a doodle jump game based on original code with using a pygame
## Table of contents
-* [General info](#general-info)
-* [Requirements](#requirements)
-* [Setup](#setup)
-
+
+
+
## General Info
-* No images used for graphics
+* Work has been done to improve the visual
* Well clean and organised code
-* Relatively small code
+* Code is optimized and performant
+* Every part of the code is uncommented
## Requirements
* [Python3](https://www.python.org/downloads/)
@@ -21,3 +21,4 @@ The simplest doodle jump made with python3 and pygame in only 2 days
* Download zip, fork or clone.
* Install requirements
* And Run !
+* If you have any problems contact me !
diff --git a/audio/effects/activate_button.ogg b/audio/effects/activate_button.ogg
new file mode 100644
index 0000000..ac49cee
Binary files /dev/null and b/audio/effects/activate_button.ogg differ
diff --git a/audio/effects/check_button.ogg b/audio/effects/check_button.ogg
new file mode 100644
index 0000000..95d61bd
Binary files /dev/null and b/audio/effects/check_button.ogg differ
diff --git a/audio/effects/explosion.ogg b/audio/effects/explosion.ogg
new file mode 100644
index 0000000..299d126
Binary files /dev/null and b/audio/effects/explosion.ogg differ
diff --git a/audio/effects/game_over.ogg b/audio/effects/game_over.ogg
new file mode 100644
index 0000000..7408419
Binary files /dev/null and b/audio/effects/game_over.ogg differ
diff --git a/audio/effects/jump.ogg b/audio/effects/jump.ogg
new file mode 100644
index 0000000..c826a1e
Binary files /dev/null and b/audio/effects/jump.ogg differ
diff --git a/audio/effects/stomp.ogg b/audio/effects/stomp.ogg
new file mode 100644
index 0000000..e902a11
Binary files /dev/null and b/audio/effects/stomp.ogg differ
diff --git a/audio/level_music.wav b/audio/level_music.wav
new file mode 100644
index 0000000..72856fc
Binary files /dev/null and b/audio/level_music.wav differ
diff --git a/audio/overworld_music.wav b/audio/overworld_music.wav
new file mode 100644
index 0000000..6fe7d74
Binary files /dev/null and b/audio/overworld_music.wav differ
diff --git a/button.py b/button.py
new file mode 100644
index 0000000..94a872b
--- /dev/null
+++ b/button.py
@@ -0,0 +1,14 @@
+import pygame
+
+# Class for management buttons in interface
+class Button(pygame.sprite.Sprite):
+ def __init__(self, image, pos, action=None):
+ super().__init__()
+ self.image = image
+ self.rect = self.image.get_rect(center=pos)
+ self.action = action # Triggered function
+
+ # Run function on mouse click
+ def check_click(self, mouse_pos):
+ if self.rect.collidepoint(mouse_pos): # If there is collision
+ self.action() # Launching
\ No newline at end of file
diff --git a/camera.py b/camera.py
index f57045e..a202a47 100644
--- a/camera.py
+++ b/camera.py
@@ -1,73 +1,37 @@
-# -*- coding: utf-8 -*-
-"""
- CopyLeft 2021 Michael Rouves
-
- This file is part of Pygame-DoodleJump.
- Pygame-DoodleJump is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Pygame-DoodleJump is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Pygame-DoodleJump. If not, see .
-"""
-
-
-from pygame import Rect
-from pygame.sprite import Sprite
-
+import pygame
from singleton import Singleton
import settings as config
-
-
class Camera(Singleton):
- """
- A class to represent the camera.
-
- Manages level position scrolling.
- Can be access via Singleton: Camera.instance.
- (Check Singleton design pattern for more info)
- """
- # constructor called on new instance: Camera()
- def __init__(self, lerp=5,width=config.XWIN, height=config.YWIN):
- self.state = Rect(0, 0, width, height)
- self.lerp = lerp
- self.center = height//2
- self.maxheight = self.center
-
- def reset(self) -> None:
- " Called only when game restarts (after player death)."
- self.state.y = 0
- self.maxheight = self.center
-
- def apply_rect(self,rect:Rect) -> Rect:
- """ Transforms given rect relative to camera position.
- :param rect pygame.Rect: the rect to transform
- """
- return rect.move((0,-self.state.topleft[1]))
-
- def apply(self, target:Sprite) -> Rect:
- """ Returns new target render position based on current camera position.
- :param target Sprite: a sprite that wants to get its render position.
- """
- return self.apply_rect(target.rect)
-
- def update(self, target:Rect) -> None:
- """ Scrolls up to maxheight reached by player.
- Should be called each frame.
- :param target pygame.Rect: the target position to follow.
- """
- # updating maxheight
- if(target.y pygame.Rect:
+ # Take a rect as input and return a new rect moved by the negative y-coordinate of the top-left point
+ return rect.move((0, -self.state.topleft[1]))
+
+ # Return a new target rendering position based on the current camera position
+ def apply(self, target: pygame.sprite.Sprite) -> pygame.Rect:
+ # Take a rect as input and a Sprite (a sprite that wants to get a rendering position)
+ # Return the required rect obtained from the apply_rect function, representing the rendering position for the target sprite
+ return self.apply_rect(target.rect)
+
+ # Method to move up to the maximum height achieved by the player
+ def update(self, target: pygame.Rect):
+ # Update the maximum height
+ if target.y < self.maxheight: # If the player reaches a new height on the screen
+ self.lastheight = self.maxheight
+ self.maxheight = target.y
+ # Calculate the required camera follow speed
+ speed = ((self.state.y + self.center) - self.maxheight) / self.lerp
+ self.state.y -= speed
diff --git a/demo.gif b/demo.gif
deleted file mode 100644
index 4e3d418..0000000
Binary files a/demo.gif and /dev/null differ
diff --git a/graphics/UI/frame.png b/graphics/UI/frame.png
new file mode 100644
index 0000000..370f152
Binary files /dev/null and b/graphics/UI/frame.png differ
diff --git a/graphics/UI/m.png b/graphics/UI/m.png
new file mode 100644
index 0000000..524de5f
Binary files /dev/null and b/graphics/UI/m.png differ
diff --git a/graphics/background/background.png b/graphics/background/background.png
new file mode 100644
index 0000000..534e900
Binary files /dev/null and b/graphics/background/background.png differ
diff --git a/graphics/bonus/defaultsituation/power_keg_with_m.png b/graphics/bonus/defaultsituation/power_keg_with_m.png
new file mode 100644
index 0000000..11c002d
Binary files /dev/null and b/graphics/bonus/defaultsituation/power_keg_with_m.png differ
diff --git a/graphics/bonus/explosion/1.png b/graphics/bonus/explosion/1.png
new file mode 100644
index 0000000..c30ea12
Binary files /dev/null and b/graphics/bonus/explosion/1.png differ
diff --git a/graphics/bonus/explosion/2.png b/graphics/bonus/explosion/2.png
new file mode 100644
index 0000000..e5f40ed
Binary files /dev/null and b/graphics/bonus/explosion/2.png differ
diff --git a/graphics/bonus/explosion/3.png b/graphics/bonus/explosion/3.png
new file mode 100644
index 0000000..c1924c8
Binary files /dev/null and b/graphics/bonus/explosion/3.png differ
diff --git a/graphics/bonus/explosion/4.png b/graphics/bonus/explosion/4.png
new file mode 100644
index 0000000..2a34561
Binary files /dev/null and b/graphics/bonus/explosion/4.png differ
diff --git a/graphics/bonus/explosion/5.png b/graphics/bonus/explosion/5.png
new file mode 100644
index 0000000..9a1dfd3
Binary files /dev/null and b/graphics/bonus/explosion/5.png differ
diff --git a/graphics/bonus/explosion/6.png b/graphics/bonus/explosion/6.png
new file mode 100644
index 0000000..d16dbc2
Binary files /dev/null and b/graphics/bonus/explosion/6.png differ
diff --git a/graphics/character/fall/2.png b/graphics/character/fall/2.png
new file mode 100644
index 0000000..ff76c31
Binary files /dev/null and b/graphics/character/fall/2.png differ
diff --git a/graphics/character/fall/3.png b/graphics/character/fall/3.png
new file mode 100644
index 0000000..35ca0ae
Binary files /dev/null and b/graphics/character/fall/3.png differ
diff --git a/graphics/character/fall/4.png b/graphics/character/fall/4.png
new file mode 100644
index 0000000..4be3134
Binary files /dev/null and b/graphics/character/fall/4.png differ
diff --git a/graphics/character/fall/5.png b/graphics/character/fall/5.png
new file mode 100644
index 0000000..c3a64e9
Binary files /dev/null and b/graphics/character/fall/5.png differ
diff --git a/graphics/character/jump/1.png b/graphics/character/jump/1.png
new file mode 100644
index 0000000..015b27a
Binary files /dev/null and b/graphics/character/jump/1.png differ
diff --git a/graphics/character/jump/2.png b/graphics/character/jump/2.png
new file mode 100644
index 0000000..2864e24
Binary files /dev/null and b/graphics/character/jump/2.png differ
diff --git a/graphics/font/1.psd b/graphics/font/1.psd
new file mode 100644
index 0000000..ec7bd34
Binary files /dev/null and b/graphics/font/1.psd differ
diff --git "a/graphics/font/1\320\277\321\200\320\276\320\265\320\272\321\202.psd" "b/graphics/font/1\320\277\321\200\320\276\320\265\320\272\321\202.psd"
new file mode 100644
index 0000000..5f1abef
Binary files /dev/null and "b/graphics/font/1\320\277\321\200\320\276\320\265\320\272\321\202.psd" differ
diff --git a/graphics/font/font.ttf b/graphics/font/font.ttf
new file mode 100644
index 0000000..98044e9
Binary files /dev/null and b/graphics/font/font.ttf differ
diff --git "a/graphics/font/\320\262\321\213\321\204\320\262\321\204\320\262\321\204\321\213\320\262.psd" "b/graphics/font/\320\262\321\213\321\204\320\262\321\204\320\262\321\204\321\213\320\262.psd"
new file mode 100644
index 0000000..a8a0d36
Binary files /dev/null and "b/graphics/font/\320\262\321\213\321\204\320\262\321\204\320\262\321\204\321\213\320\262.psd" differ
diff --git a/graphics/game_over_menu/buttons/active/main_menu.png b/graphics/game_over_menu/buttons/active/main_menu.png
new file mode 100644
index 0000000..8d7ab7d
Binary files /dev/null and b/graphics/game_over_menu/buttons/active/main_menu.png differ
diff --git a/graphics/game_over_menu/buttons/active/restart.png b/graphics/game_over_menu/buttons/active/restart.png
new file mode 100644
index 0000000..c8f4ddf
Binary files /dev/null and b/graphics/game_over_menu/buttons/active/restart.png differ
diff --git a/graphics/game_over_menu/buttons/non_active/main_menu.png b/graphics/game_over_menu/buttons/non_active/main_menu.png
new file mode 100644
index 0000000..a00cafd
Binary files /dev/null and b/graphics/game_over_menu/buttons/non_active/main_menu.png differ
diff --git a/graphics/game_over_menu/buttons/non_active/restart.png b/graphics/game_over_menu/buttons/non_active/restart.png
new file mode 100644
index 0000000..b41213b
Binary files /dev/null and b/graphics/game_over_menu/buttons/non_active/restart.png differ
diff --git a/graphics/game_over_menu/game_over.png b/graphics/game_over_menu/game_over.png
new file mode 100644
index 0000000..078594d
Binary files /dev/null and b/graphics/game_over_menu/game_over.png differ
diff --git a/graphics/main_menu/background/background.png b/graphics/main_menu/background/background.png
new file mode 100644
index 0000000..ad84ae1
Binary files /dev/null and b/graphics/main_menu/background/background.png differ
diff --git a/graphics/main_menu/button/active/exit_btn.png b/graphics/main_menu/button/active/exit_btn.png
new file mode 100644
index 0000000..fe29548
Binary files /dev/null and b/graphics/main_menu/button/active/exit_btn.png differ
diff --git a/graphics/main_menu/button/active/start_btn.png b/graphics/main_menu/button/active/start_btn.png
new file mode 100644
index 0000000..d77d6b5
Binary files /dev/null and b/graphics/main_menu/button/active/start_btn.png differ
diff --git a/graphics/main_menu/button/non_active/exit_btn.png b/graphics/main_menu/button/non_active/exit_btn.png
new file mode 100644
index 0000000..d6eb676
Binary files /dev/null and b/graphics/main_menu/button/non_active/exit_btn.png differ
diff --git a/graphics/main_menu/button/non_active/start_btn.png b/graphics/main_menu/button/non_active/start_btn.png
new file mode 100644
index 0000000..4677714
Binary files /dev/null and b/graphics/main_menu/button/non_active/start_btn.png differ
diff --git a/graphics/main_menu/marwin.png b/graphics/main_menu/marwin.png
new file mode 100644
index 0000000..7f5f48f
Binary files /dev/null and b/graphics/main_menu/marwin.png differ
diff --git a/graphics/pause/buttons/active/resume.png b/graphics/pause/buttons/active/resume.png
new file mode 100644
index 0000000..14e43cd
Binary files /dev/null and b/graphics/pause/buttons/active/resume.png differ
diff --git a/graphics/pause/buttons/non_active/resume.png b/graphics/pause/buttons/non_active/resume.png
new file mode 100644
index 0000000..e2b28c7
Binary files /dev/null and b/graphics/pause/buttons/non_active/resume.png differ
diff --git a/graphics/pause/frame.png b/graphics/pause/frame.png
new file mode 100644
index 0000000..af4bab6
Binary files /dev/null and b/graphics/pause/frame.png differ
diff --git a/graphics/platform/broken/broken-platform.png b/graphics/platform/broken/broken-platform.png
new file mode 100644
index 0000000..c614f74
Binary files /dev/null and b/graphics/platform/broken/broken-platform.png differ
diff --git a/graphics/platform/normal/platform.png b/graphics/platform/normal/platform.png
new file mode 100644
index 0000000..de78b36
Binary files /dev/null and b/graphics/platform/normal/platform.png differ
diff --git a/level.py b/level.py
index 5cb6954..d1c542b 100644
--- a/level.py
+++ b/level.py
@@ -1,211 +1,143 @@
-# -*- coding: utf-8 -*-
-"""
- CopyLeft 2021 Michael Rouves
-
- This file is part of Pygame-DoodleJump.
- Pygame-DoodleJump is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Pygame-DoodleJump is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Pygame-DoodleJump. If not, see .
-"""
-
-
-from random import randint
-from pygame import Surface
-import asyncio
-
-from singleton import Singleton
-from sprite import Sprite
-import settings as config
-
-
-
-#return True with a chance of: P(X=True)=1/x
-chance = lambda x: not randint(0,x)
-
-
-class Bonus(Sprite):
- """
- A class to represent a bonus
- Inherits the Sprite class.
- """
-
- WIDTH = 15
- HEIGHT = 15
-
- def __init__(self, parent:Sprite,color=config.GRAY,
- force=config.PLAYER_BONUS_JUMPFORCE):
-
- self.parent = parent
- super().__init__(*self._get_inital_pos(),
- Bonus.WIDTH, Bonus.HEIGHT, color)
- self.force = force
-
- def _get_inital_pos(self):
- x = self.parent.rect.centerx - Bonus.WIDTH//2
- y = self.parent.rect.y - Bonus.HEIGHT
- return x,y
-
-
-
-
-
-class Platform(Sprite):
- """
- A class to represent a platform.
-
- Should only be instantiated by a Level instance.
- Can have a bonus spring or broke on player jump.
- Inherits the Sprite class.
- """
- # (Overriding inherited constructor: Sprite.__init__)
- def __init__(self, x:int, y:int, width:int, height:int,
- initial_bonus=False,breakable=False):
-
- color = config.PLATFORM_COLOR
- if breakable:color = config.PLATFORM_COLOR_LIGHT
- super().__init__(x,y,width,height,color)
-
- self.breakable = breakable
- self.__level = Level.instance
- self.__bonus = None
- if initial_bonus:
- self.add_bonus(Bonus)
-
- # Public getter for __bonus so it remains private
- @property
- def bonus(self):return self.__bonus
-
- def add_bonus(self,bonus_type:type) -> None:
- """ Safely adds a bonus to the platform.
- :param bonus_type type: the type of bonus to add.
- """
- assert issubclass(bonus_type,Bonus), "Not a valid bonus type !"
- if not self.__bonus and not self.breakable:
- self.__bonus = bonus_type(self)
-
- def remove_bonus(self) -> None:
- " Safely removes platform's bonus."
- self.__bonus = None
-
- def onCollide(self) -> None:
- " Called in update if collision with player (safe to overrided)."
- if self.breakable:
- self.__level.remove_platform(self)
-
- # ( Overriding inheritance: Sprite.draw() )
- def draw(self, surface:Surface) -> None:
- """ Like Sprite.draw().
- Also draws the platform's bonus if it has one.
- :param surface pygame.Surface: the surface to draw on.
- """
- # check if out of screen: should be deleted
- super().draw(surface)
- if self.__bonus:
- self.__bonus.draw(surface)
- if self.camera_rect.y+self.rect.height>config.YWIN:
- self.__level.remove_platform(self)
-
-
-
-
-
-class Level(Singleton):
- """
- A class to represent the level.
-
- used to manage updates/generation of platforms.
- Can be access via Singleton: Level.instance.
- (Check Singleton design pattern for more info)
- """
-
- # constructor called on new instance: Level()
- def __init__(self):
- self.platform_size = config.PLATFORM_SIZE
- self.max_platforms = config.MAX_PLATFORM_NUMBER
- self.distance_min = min(config.PLATFORM_DISTANCE_GAP)
- self.distance_max = max(config.PLATFORM_DISTANCE_GAP)
-
- self.bonus_platform_chance = config.BONUS_SPAWN_CHANCE
- self.breakable_platform_chance = config.BREAKABLE_PLATFORM_CHANCE
-
- self.__platforms = []
- self.__to_remove = []
-
- self.__base_platform = Platform(
- config.HALF_XWIN - self.platform_size[0]//2,# X POS
- config.HALF_YWIN + config.YWIN/3, # Y POS
- *self.platform_size)# SIZE
-
-
- # Public getter for __platforms so it remains private
- @property
- def platforms(self) -> list:
- return self.__platforms
-
-
- async def _generation(self) -> None:
- " Asynchronous management of platforms generation."
- # Check how many platform we need to generate
- nb_to_generate = self.max_platforms - len(self.__platforms)
- for _ in range(nb_to_generate):
- self.create_platform()
-
-
- def create_platform(self) -> None:
- " Create the first platform or a new one."
- if self.__platforms:
- # Generate a new random platform :
- # x position along screen width
- # y position starting from last platform y pos +random offset
- offset = randint(self.distance_min,self.distance_max)
- self.__platforms.append(Platform(
- randint(0,config.XWIN-self.platform_size[0]),# X POS
- self.__platforms[-1].rect.y-offset,# Y POS
- *self.platform_size, # SIZE
- initial_bonus=chance(self.bonus_platform_chance),# HAS A Bonus
- breakable=chance(self.breakable_platform_chance)))# IS BREAKABLE
- else:
- # (just in case) no platform: add the base one
- self.__platforms.append(self.__base_platform)
-
-
- def remove_platform(self,plt:Platform) -> bool:
- """ Removes a platform safely.
- :param plt Platform: the platform to remove
- :return bool: returns true if platoform successfully removed
- """
- if plt in self.__platforms:
- self.__to_remove.append(plt)
- return True
- return False
-
-
- def reset(self) -> None:
- " Called only when game restarts (after player death)."
- self.__platforms = [self.__base_platform]
-
-
- def update(self) -> None:
- " Should be called each frame in main game loop for generation."
- for platform in self.__to_remove:
- if platform in self.__platforms:
- self.__platforms.remove(platform)
- self.__to_remove = []
- asyncio.run(self._generation())
-
-
- def draw(self,surface:Surface) -> None:
- """ Called each frame in main loop, draws each platform
- :param surface pygame.Surface: the surface to draw on.
- """
- for platform in self.__platforms:
- platform.draw(surface)
\ No newline at end of file
+from random import randint
+from pygame import Surface, image, transform, mixer
+from pygame.time import Clock, get_ticks
+import asyncio
+import settings as config
+from singleton import Singleton
+from sprite import Sprite
+mixer.init()
+
+chance = lambda x: not randint(0,x) # Creating a variable dependent on x, the more x, the less likely it is to get True
+class Bonus(Sprite):
+ def __init__(self, parent:Sprite, force=config.PLAYER_BONUS_JUMPFORCE): # Accepting the parameters of the parent object
+ self.parent = parent # Binding
+ super().__init__(*self._get_inital_pos(),
+ config.BONUS_WIDTH, config.BONUS_HEIGHT) # Position, width, height, color are passed
+ self.force = force # Jump power
+ self.model = image.load(config.bonus_default_image).convert_alpha() # Image model
+ self.effect = False # If player speed increase effect occurs (used to start animation)
+ self.tag = 'bonus' # tag
+ self.bonus_effect_duration = 100 # Bonus animation effect duration
+ self.bonus_effect_start_time = None # Blank for counting the start of the animation
+ self.current_frame = 0 # Current animation frame
+ self.animation_finished = False # Animation ended
+
+ def _get_inital_pos(self):
+ # Getting the current position
+ x = self.parent.rect.centerx - config.BONUS_WIDTH//2
+ y = self.parent.rect.y - config.BONUS_HEIGHT
+ return x,y
+
+class Platform(Sprite):
+ def __init__(self, x:int, y:int, width:int, height:int,
+ initial_bonus=False,breakable=False): # designation of data types and initial parameters in the class constructor
+ self.model = image.load(config.platform_image).convert_alpha() # model (default)
+ self.tag = 'platform' # tag
+
+ if breakable: # Model change if the platform is disposable
+ self.model = image.load(config.broken_platform_image).convert_alpha() # model (broken)
+
+ super().__init__(x,y,width,height)
+ self.breakable = breakable
+ self.__level = Level.instance # Setting value equal to class instance Level
+ self.__bonus = None # Blank for creating a personal instance of the Bonus class
+ if initial_bonus: # If the platform have a bonus
+ self.add_bonus(Bonus) # Adding in a sprite group
+
+ @property # The ability to open access the class
+ def bonus(self):
+ return self.__bonus
+
+ def add_bonus(self,bonus_type:type):
+ # Adds a bonus to the platform
+ assert issubclass(bonus_type,Bonus) # Checking if bonus_type is a subclass of Bonus
+ if not self.__bonus and not self.breakable: # Checking whether there is already a bonus on the platform and whether it is a solid platform
+ self.__bonus = bonus_type(self) # If the conditions match, a bonus instance of type bonus_type is created
+
+ def remove_bonus(self):
+ self.__bonus = None
+
+ def onCollide(self):
+ # Removal of the platform if it is a one-time use
+ if self.breakable:
+ config.stomp.play() # Breaking sound production
+ self.__level.remove_platform(self)
+
+ def draw(self, surface:Surface):
+ # Rendering the platform on the passed Surface
+ super().draw(surface) # Draw method call
+ distance = 0
+ if self.__bonus: # Checking for the existence of an object on the platform
+ self.__bonus.draw(surface)
+ distance = config.BONUS_HEIGHT # Bonus amount
+ if self.camera_rect.y + self.rect.height > config.YWIN + distance + 5: # Checking if the platform boundary goes beyond the screen
+ self.__level.remove_platform(self) # Removing a platform
+
+class Level(Singleton):
+ def __init__(self):
+ self.platform_size = config.PLATFORM_SIZE # Platform size
+ self.max_platforms = config.MAX_PLATFORM_NUMBER # Max platform count
+ self.distance_min = min(config.PLATFORM_DISTANCE_GAP) # Minimum distance between platforms
+ self.distance_max = max(config.PLATFORM_DISTANCE_GAP) # Maximum distance between platforms
+
+
+ self.bonus_platform_chance = config.BONUS_SPAWN_CHANCE # Bonus spawn chance
+ self.breakable_platform_chance = config.BREAKABLE_PLATFORM_CHANCE # Disposable platform chance
+
+ self.__platforms = []
+ self.__to_remove = [] # Whatever needs to be removed
+
+ # Creating a platform instance
+ self.__base_platform = Platform(
+ config.HALF_XWIN - self.platform_size[0]//2, # X pos
+ config.HALF_YWIN + config.YWIN/3, # Y pos
+ *self.platform_size) # Size
+
+ # Definition as a class property
+ @property
+ def platforms(self) -> list: # Expected data type: list
+ return self.__platforms # Returns all platforms as list
+
+ # Asynchronous function (asynchrony is used to optimize and process the function by several processor cores)
+ async def _generation(self):
+ # Checking the required number of platforms for generation
+ nb_to_generate = self.max_platforms - len(self.__platforms) # Calculation of the number of platforms for generation
+ for _ in range(nb_to_generate): # Creating a platforms
+ self.create_platform()
+
+ def create_platform(self):
+ # Creating a new platforms
+ if self.__platforms: # If platform exists
+ # Generation a random platform
+ offset = randint(self.distance_min,self.distance_max) # Mixing relative to neighbors
+ self.__platforms.append(Platform( # Creating a new instance
+ randint(0,config.XWIN-self.platform_size[0]), # X pos
+ self.__platforms[-1].rect.y-offset, # Y pos
+ *self.platform_size, # Size
+ initial_bonus=chance(self.bonus_platform_chance), # Bonus available
+ breakable=chance(self.breakable_platform_chance))) # Disposable or not
+ else:
+ # If the platform does not exist, adds the base platform to the list
+ self.__platforms.append(self.__base_platform)
+
+ def remove_platform(self,plt:Platform) -> bool: # Return value bool
+ if plt in self.__platforms:
+ self.__to_remove.append(plt)
+ return True
+ return False # Platform has not found
+
+ def reset(self):
+ # Platform reloading, by assigning platforms to one base platform
+ self.__platforms = [self.__base_platform]
+
+ def update(self):
+ # Called every frame to generate
+ for platform in self.__to_remove:
+ if platform in self.__platforms:
+ self.__platforms.remove(platform)
+ self.__to_remove = []
+ asyncio.run(self._generation()) # Running a function using async
+
+ def draw(self,surface:Surface):
+ for platform in self.__platforms: # Rendering of each platform
+ platform.draw(surface)
diff --git a/main.py b/main.py
index 727970f..553e22e 100644
--- a/main.py
+++ b/main.py
@@ -1,135 +1,330 @@
-# -*- coding: utf-8 -*-
-"""
- CopyLeft 2021 Michael Rouves
-
- This file is part of Pygame-DoodleJump.
- Pygame-DoodleJump is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Pygame-DoodleJump is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Pygame-DoodleJump. If not, see .
-"""
-
-
+from MARWIN_ADVENTURE import *
+from button import Button
import pygame, sys
-
from singleton import Singleton
from camera import Camera
from player import Player
from level import Level
import settings as config
+pygame.init()
+# Window
+window = pygame.display.set_mode(config.DISPLAY,config.FLAGS)
+clock = pygame.time.Clock()
+# Function for displaying text
+def display_text(text, font_size, color, font_path, pos):
+ font = pygame.font.Font(font_path, font_size)
+ text_render = font.render(text, True, color)
+ text_rect = text_render.get_rect(center=pos)
+ window.blit(text_render, text_rect)
class Game(Singleton):
- """
- A class to represent the game.
+ # Class for managing the game
+ def __init__(self):
+ # Initialization
+ self.score = 0 # Record
+ self.paused = False # Pause
+ self.max_darkness_reached = False # Has maximum darkness been reached
+ self.frame_reverse_animation_end = True # End of reverse frame animation
+ self.frame_animation_end = False # End of frame animation
+ self.game_over_animation_end = False # End of game over image animation
+ # End of button animation
+ self.mainmenu_button_animation_end = False
+ self.restart_button_animation_end = False
- used to manage game updates, draw calls and user input events.
- Can be access via Singleton: Game.instance .
- (Check Singleton design pattern for more info)
- """
+ # Game over
+ self.game_over_image = pygame.image.load(config.game_over_image).convert()
+ self.gameover_rect_initial_y = -config.HALF_YWIN # Initial position of game over vertically
+ self.gameover_rect_y = self.gameover_rect_initial_y # Current offset of game over vertically
+ self.gameover_rect = self.game_over_image.get_rect(center=(config.HALF_XWIN,config.HALF_YWIN//3)) # Get image size
+ self.game_over_sound_played = False
- # constructor called on new instance: Game()
- def __init__(self) -> None:
+ # Frame images for pause, score
+ self.frame_image_pause = pygame.image.load(config.frame_image_pause).convert()
+ self.frame_image_score = pygame.image.load(config.frame_image_score).convert()
+ self.frame_rect_initial_y = -20 # Initial position of frame vertically
+ self.frame_rect_y = self.frame_rect_initial_y # Current offset of frame vertically
- # ============= Initialisation =============
- self.__alive = True
- # Window / Render
- self.window = pygame.display.set_mode(config.DISPLAY,config.FLAGS)
- self.clock = pygame.time.Clock()
-
- # Instances
- self.camera = Camera()
- self.lvl = Level()
- self.player = Player(
- config.HALF_XWIN - config.PLAYER_SIZE[0]/2,# X POS
- config.HALF_YWIN + config.HALF_YWIN/2,# Y POS
- *config.PLAYER_SIZE,# SIZE
- config.PLAYER_COLOR# COLOR
- )
-
- # User Interface
- self.score = 0
- self.score_txt = config.SMALL_FONT.render("0 m",1,config.GRAY)
- self.score_pos = pygame.math.Vector2(10,10)
-
- self.gameover_txt = config.LARGE_FONT.render("Game Over",1,config.GRAY)
- self.gameover_rect = self.gameover_txt.get_rect(
- center=(config.HALF_XWIN,config.HALF_YWIN))
-
-
- def close(self):
- self.__alive = False
+ # Buttons: mainmenu, restart, resume
+ self.mainmenu_button_rect_initial_x = 0 # Initial position of mainmenu horizontally
+ self.restart_button_rect_initial_x = config.XWIN # Initial position of restart horizontally
+ self.mainmenu_button_image = pygame.image.load(config.mainmenu_button_non_active)
+ self.restart_button_image = pygame.image.load(config.restart_button_non_active)
+ self.resume_button_image = pygame.image.load(config.resume_button_non_active)
+ # Button sounds
+ self.mainmenu_button_sound = False
+ self.restart_button_sound = False
+ self.resume_button_sound = False
- def reset(self):
- self.camera.reset()
- self.lvl.reset()
- self.player.reset()
+ # Background
+ self.background_image = pygame.image.load(config.background_image).convert()
+ self.background_image = pygame.transform.scale(self.background_image, (config.XWIN, config.YWIN))
+ self.overlay_color = config.BLACK # Dimming color
+ self.overlay_alpha = 200 # Dimming alpha channel
+ ######################Class instances######################
+ # Buttons
+ #For game over
+ self.mainmenu_button = Button(self.mainmenu_button_image, (self.mainmenu_button_rect_initial_x, config.YWIN // 1.2), action='')
+ self.restart_button = Button(self.restart_button_image, (self.restart_button_rect_initial_x, config.YWIN // 1.2), action='')
+ # For pause
+ self.mainmenu_button_pause = Button(self.mainmenu_button_image, (config.XWIN // 4, config.YWIN // 2), action='')
+ self.restart_button_pause = Button(self.restart_button_image, (config.XWIN // 2, config.YWIN // 2), action='')
+ self.resume_button = Button(self.resume_button_image, (config.XWIN // 1.34, config.YWIN // 2), action='')
+ # Objects
+ self.camera = Camera() # Camera
+ self.lvl = Level() # Level
+ self.player = Player( # Player
+ config.HALF_XWIN - config.PLAYER_SIZE[0]/2, # X position
+ config.HALF_YWIN + config.HALF_YWIN/2, # Y position
+ *config.PLAYER_SIZE,) # Size
+
+ def resume(self): # Resume pause
+ self.paused = False
+
+ def reset(self): # Reset the game
+ if self.game_over_animation_end or self.paused:
+ pygame.mixer.music.play(-1)
+ self.paused = False
+ self.camera.reset()
+ self.lvl.reset()
+ self.player.reset()
+
+ def launch_main_menu(self): # Launch main menu and exit current loop
+ import MARWIN_ADVENTURE
+ MARWIN_ADVENTURE.Run()
+ pygame.quit()
+ sys.exit()
- def _event_loop(self):
- # ---------- User Events ----------
+ def _event_loop(self): # Event handling
+ # User events
+ mouse_pos = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
- self.close()
- elif event.type == pygame.KEYDOWN:
- if event.key == pygame.K_ESCAPE:
- self.close()
- if event.key == pygame.K_RETURN and self.player.dead:
- self.reset()
- self.player.handle_event(event)
+ #self.close()
+ pygame.quit()
+ sys.exit()
+ elif event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_ESCAPE: # Pause
+ self.paused = not self.paused # Toggle value
+ elif event.type == pygame.MOUSEBUTTONDOWN:
+ if self.player.dead: # If game over, process menu buttons to prevent false clicks
+ self.mainmenu_button.check_click(mouse_pos)
+ self.restart_button.check_click(mouse_pos)
+ if self.paused: # If paused, process pause menu buttons
+ self.resume_button.check_click(mouse_pos)
+ self.mainmenu_button_pause.check_click(mouse_pos)
+ self.restart_button_pause.check_click(mouse_pos)
+ if not self.paused: # If not paused, process user events
+ self.player.handle_event(event)
def _update_loop(self):
- # ----------- Update -----------
+ # Update surfaces
self.player.update()
self.lvl.update()
if not self.player.dead:
- self.camera.update(self.player.rect)
- #calculate score and update UI txt
- self.score=-self.camera.state.y//50
- self.score_txt = config.SMALL_FONT.render(
- str(self.score)+" m", 1, config.GRAY)
-
+ self.camera.update(self.player.rect) # Update camera movement
+ # Calculate score and display it
+ self.score=-self.camera.state.y//50 # Update score
def _render_loop(self):
- # ----------- Display -----------
- self.window.fill(config.WHITE)
- self.lvl.draw(self.window)
- self.player.draw(self.window)
+ global window, clock
+ # Display objects
- # User Interface
- if self.player.dead:
- self.window.blit(self.gameover_txt,self.gameover_rect)# gameover txt
- self.window.blit(self.score_txt, self.score_pos)# score txt
+ # Calculate background offset based on camera position
+ offset_y = -self.camera.state.y % config.YWIN
+ # Display background
+ window.blit(self.background_image, (0, offset_y))
+ window.blit(self.background_image, (0, offset_y - config.YWIN))
- pygame.display.update()# window update
- self.clock.tick(config.FPS)# max loop/s
+ # Draw objects using Sprite.py
+ self.lvl.draw(window)
+ self.player.draw(window)
+ # Assign functions to buttons
+ # Buttons for game over
+ self.mainmenu_button.action = self.launch_main_menu
+ self.restart_button.action = self.reset
+ # Buttons for pause
+ self.resume_button.action = self.resume
+ self.mainmenu_button_pause.action = self.launch_main_menu
+ self.restart_button_pause.action = self.reset
+
+ ################## DRAWING OBJECTS BASED ON EVENTS ##################
+ if not self.player.dead and self.paused: # Pause
+ mouse_pos = pygame.mouse.get_pos() # Get current mouse position
+ window.blit(self.frame_image_pause, (pygame.math.Vector2(config.HALF_XWIN - 185, config.YWIN // 4 - 50))) # Draw frame for text
+ display_text('PAUSE', 65, config.WHITE, config.pixel_font, (pygame.math.Vector2(config.HALF_XWIN, config.YWIN // 4))) # Draw "PAUSE" text
+
+ # Handle button hover reactions
+ # Resume button
+ if self.resume_button.rect.collidepoint(mouse_pos): # If collision, switch to active image
+ if self.resume_button_sound: # Play button sound only once
+ config.check_button.play()
+ self.resume_button_sound = False
+ window.blit(pygame.image.load(config.resume_button_active).convert(), self.resume_button.rect)
+ else: # Change to default image
+ self.resume_button_sound = True
+ window.blit(self.resume_button.image.convert(), self.resume_button.rect)
- def run(self):
- # ============= MAIN GAME LOOP =============
- while self.__alive:
- self._event_loop()
- self._update_loop()
- self._render_loop()
- pygame.quit()
+ # Main menu
+ if self.mainmenu_button_pause.rect.collidepoint(mouse_pos): # If there is collision, change to active image
+ if self.mainmenu_button_sound: # Play button sound only once
+ config.check_button.play()
+ self.mainmenu_button_sound = False
+ window.blit(pygame.image.load(config.mainmenu_button_active).convert(), self.mainmenu_button_pause.rect)
+ else: # Change to default image
+ self.mainmenu_button_sound = True
+ window.blit(self.mainmenu_button_pause.image.convert(), self.mainmenu_button_pause.rect)
+ # Restart
+ if self.restart_button_pause.rect.collidepoint(mouse_pos): # If there is collision, change to active image
+ if self.restart_button_sound: # Play button sound only once
+ config.check_button.play()
+ self.restart_button_sound = False
+ window.blit(pygame.image.load(config.restart_button_active).convert(), self.restart_button_pause.rect)
+ else: # Change to default image
+ self.restart_button_sound = True
+ window.blit(self.restart_button_pause.image.convert(), self.restart_button_pause.rect)
+ if self.player.dead: # Game over
+ # Zeroing the results of the frame if the animation is over
+ if self.frame_animation_end:
+ self.frame_rect_y = self.frame_rect_initial_y
+ self.frame_animation_end = False
+ self.frame_reverse_animation_end = False
+ if config.music_volume >= 0.0:
+ config.music_volume -= 0.001
+ pygame.mixer.music.set_volume(config.music_volume)
-if __name__ == "__main__":
- # ============= PROGRAM STARTS HERE =============
+ # Screen dimming
+ self.overlay_alpha = min(self.overlay_alpha + 5, 200)
+ if self.overlay_alpha >= 200:
+ self.max_darkness_reached = True
+
+ # Animation of moving buttons and images
+ # Game over
+ if self.gameover_rect_y < 15:
+ self.gameover_rect_y += 10
+ else:
+ self.game_over_animation_end = True
+
+ # Main menu button
+ if self.mainmenu_button.rect.x < config.XWIN // 3 - self.mainmenu_button.rect[2] // 2:
+ self.mainmenu_button.rect.x += 10
+ else:
+ self.mainmenu_button_animation_end = True
+
+ # Restart button
+ if self.restart_button.rect.x > config.XWIN // 1.5 - self.restart_button.rect[2] // 2:
+ self.restart_button.rect.x -= 13
+ else:
+ self.restart_button_animation_end = True
+
+ else:
+ # Zeroing values
+ # Decrease the alpha channel of the fade over time (this is where the fade rate is set)
+ if config.music_volume < 0.1:
+ config.music_volume += 0.01
+ pygame.mixer.music.set_volume(config.music_volume)
+ self.game_over_sound_played = False
+ self.overlay_alpha = max(self.overlay_alpha - 5, 0)
+ self.max_darkness_reached = False
+ self.game_over_animation_end = False
+ self.mainmenu_button_animation_end = False
+ self.restart_button_animation_end = False
+ self.score_txt_visible = True
+ self.gameover_rect_y = self.gameover_rect_initial_y
+ self.mainmenu_button.rect.x = self.mainmenu_button_rect_initial_x
+ self.restart_button.rect.x = self.restart_button_rect_initial_x
+
+ # Creating a surface and applying shading to it
+ overlay_surface = pygame.Surface((config.XWIN, config.YWIN))
+ overlay_surface.fill(self.overlay_color)
+ overlay_surface.set_alpha(self.overlay_alpha)
+ if self.overlay_alpha != 0:
+ self.score_txt_visible = False
+ window.blit(overlay_surface, (0, 0))
+
+ # Game process
+ if self.score_txt_visible or not self.frame_reverse_animation_end:
+ # High score counter animation
+ if not self.frame_animation_end and self.score_txt_visible:
+ if self.frame_rect_y < 10:
+ self.frame_rect_y += 2
+ else:
+ self.frame_animation_end = True
+
+ # High score counter animation
+ elif not self.frame_reverse_animation_end:
+ if self.frame_rect_y >= -50:
+ self.frame_rect_y -= 10
+ else:
+ self.frame_reverse_animation_end = True
+
+ # Displaying
+ window.blit(self.frame_image_score, (config.XWIN // 6, self.frame_rect_y)) # Frame
+ display_text(str(self.score) + "m", 30, config.WHITE, config.pixel_font, (pygame.math.Vector2(config.XWIN // 3.1, self.frame_rect_y + 25))) # Score text
+
+ # Displaying game over objects
+ if self.max_darkness_reached: # if darkening ended
+ if not self.game_over_sound_played:
+ config.game_over.play()
+ self.game_over_sound_played = True
+
+ window.blit(self.game_over_image, (config.HALF_XWIN - self.gameover_rect.width // 2, self.gameover_rect_y)) # Displaying game over images
+ display_text(" SCORE:" + str(self.score) + "m", 40, config.WHITE, config.pixel_font, (config.HALF_XWIN, self.gameover_rect_y + config.YWIN // 1.5)) # Displaying score
+ mouse_pos = pygame.mouse.get_pos() # Getting mouse position
+
+ # Processing the reaction of buttons on hovering the cursor
+ # Main menu
+ if self.mainmenu_button.rect.collidepoint(mouse_pos) and self.game_over_animation_end: # If there is collision, change to active image
+ if self.mainmenu_button_sound: # Play button sound only once
+ config.check_button.play()
+ self.mainmenu_button_sound = False
+ window.blit(pygame.image.load(config.mainmenu_button_active).convert(), self.mainmenu_button.rect)
+ else: # Change to default image
+ self.mainmenu_button_sound = True
+ window.blit(self.mainmenu_button.image.convert(), self.mainmenu_button.rect)
+
+ # Restart
+ if self.restart_button.rect.collidepoint(mouse_pos) and self.game_over_animation_end: # If there is collision, change to active image
+ if self.restart_button_sound: # Play button sound only once
+ config.check_button.play()
+ self.restart_button_sound = False
+ window.blit(pygame.image.load(config.restart_button_active).convert(), self.restart_button.rect)
+ else: # Change to default image
+ self.restart_button_sound = True
+ window.blit(self.restart_button.image.convert(), self.restart_button.rect)
+
+ # Updating scene
+ pygame.display.update()
+ clock.tick(config.FPS)
+
+ def run(self):
+ # Main game loop
+ pygame.mixer.music.load(config.level_music)
+ pygame.mixer.music.set_volume(config.music_volume)
+ pygame.mixer.music.play(-1)
+ while True:
+ self._event_loop() # Event handling
+ self._render_loop() # Event render
+ if not self.paused:
+ self._update_loop() # Update surface
+
+def run_game(): # Game launch function
game = Game()
game.run()
+if __name__ == "__main__":
+ # Program start
+ run_game()
+
+
+
+
diff --git a/player.py b/player.py
index 4b80fa8..e16fe67 100644
--- a/player.py
+++ b/player.py
@@ -1,153 +1,128 @@
-# -*- coding: utf-8 -*-
-"""
- CopyLeft 2021 Michael Rouves
-
- This file is part of Pygame-DoodleJump.
- Pygame-DoodleJump is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Pygame-DoodleJump is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Pygame-DoodleJump. If not, see .
-"""
-
-
from math import copysign
-from pygame.math import Vector2
-from pygame.locals import KEYDOWN,KEYUP,K_LEFT,K_RIGHT
-from pygame.sprite import collide_rect
-from pygame.event import Event
-
+import pygame
from singleton import Singleton
from sprite import Sprite
from level import Level
import settings as config
+pygame.mixer.init()
-
-#Return the sign of a number: getsign(-5)-> -1
-getsign = lambda x : copysign(1, x)
+# Returning values: (x < 0 => -1) & (x > 0 => 1) & (x == 0 => 0)
+getsign = lambda x: copysign(1, x)
class Player(Sprite, Singleton):
- """
- A class to represent the player.
-
- Manages player's input,physics (movement...).
- Can be access via Singleton: Player.instance.
- (Check Singleton design pattern for more info).
- """
- # (Overriding Sprite.__init__ constructor)
- def __init__(self,*args):
- #calling default Sprite constructor
- Sprite.__init__(self,*args)
- self.__startrect = self.rect.copy()
- self.__maxvelocity = Vector2(config.PLAYER_MAX_SPEED,100)
- self.__startspeed = 1.5
-
- self._velocity = Vector2()
- self._input = 0
- self._jumpforce = config.PLAYER_JUMPFORCE
- self._bonus_jumpforce = config.PLAYER_BONUS_JUMPFORCE
-
- self.gravity = config.GRAVITY
- self.accel = .5
- self.deccel = .6
- self.dead = False
-
-
- def _fix_velocity(self) -> None:
- """ Set player's velocity between max/min.
- Should be called in Player.update().
- """
- self._velocity.y = min(self._velocity.y,self.__maxvelocity.y)
- self._velocity.y = round(max(self._velocity.y,-self.__maxvelocity.y),2)
- self._velocity.x = min(self._velocity.x,self.__maxvelocity.x)
- self._velocity.x = round(max(self._velocity.x,-self.__maxvelocity.x),2)
-
-
- def reset(self) -> None:
- " Called only when game restarts (after player death)."
- self._velocity = Vector2()
- self.rect = self.__startrect.copy()
- self.camera_rect = self.__startrect.copy()
- self.dead = False
-
-
- def handle_event(self,event:Event) -> None:
- """ Called in main loop foreach user input event.
- :param event pygame.Event: user input event
- """
- # Check if start moving
- if event.type == KEYDOWN:
- # Moves player only on x-axis (left/right)
- if event.key == K_LEFT:
- self._velocity.x=-self.__startspeed
- self._input = -1
- elif event.key == K_RIGHT:
- self._velocity.x=self.__startspeed
- self._input = 1
- #Check if stop moving
- elif event.type == KEYUP:
- if (event.key== K_LEFT and self._input==-1) or (
- event.key==K_RIGHT and self._input==1):
- self._input = 0
-
-
- def jump(self,force:float=None) -> None:
- if not force:force = self._jumpforce
- self._velocity.y = -force
-
-
- def onCollide(self, obj:Sprite) -> None:
- self.rect.bottom = obj.rect.top
- self.jump()
-
-
- def collisions(self) -> None:
- """ Checks for collisions with level.
- Should be called in Player.update().
- """
- lvl = Level.instance
- if not lvl: return
- for platform in lvl.platforms:
- # check falling and colliding <=> isGrounded ?
- if self._velocity.y > .5:
- # check collisions with platform's spring bonus
- if platform.bonus and collide_rect(self,platform.bonus):
- self.onCollide(platform.bonus)
- self.jump(platform.bonus.force)
-
- # check collisions with platform
- if collide_rect(self,platform):
- self.onCollide(platform)
- platform.onCollide()
-
-
- def update(self) -> None:
- """ For position and velocity updates.
- Should be called each frame.
- """
- #Check if player out of screen: should be dead
- if self.camera_rect.y>config.YWIN*2:
- self.dead = True
- return
- #Velocity update (apply gravity, input acceleration)
- self._velocity.y += self.gravity
- if self._input: # accelerate
- self._velocity.x += self._input*self.accel
- elif self._velocity.x: # deccelerate
- self._velocity.x -= getsign(self._velocity.x)*self.deccel
- self._velocity.x = round(self._velocity.x)
- self._fix_velocity()
-
- #Position Update (prevent x-axis to be out of screen)
- self.rect.x = (self.rect.x+self._velocity.x)%(config.XWIN-self.rect.width)
- self.rect.y += self._velocity.y
-
- self.collisions()
\ No newline at end of file
+ # Initialization using a shared dictionary
+ def __init__(self, *args):
+ Sprite.__init__(self, *args)
+ self.__startrect = self.rect.copy() # Create the main rect
+ self.__maxvelocity = pygame.Vector2(config.PLAYER_MAX_SPEED, 100) # Create a vector with x, y coordinates
+ self.__startspeed = 1.5 # Initial speed, increases over time
+ self._velocity = pygame.Vector2() # Create a vector
+ self._input = 0 # Input check
+ self._jumpforce = config.PLAYER_JUMPFORCE # Jump force
+ self._bonus_jumpforce = config.PLAYER_BONUS_JUMPFORCE # Bonus jump force
+ self.gravity = config.GRAVITY # Gravity
+ self.accel = 0.5 # Acceleration
+ self.deccel = 0.6 # Deceleration
+ self.dead = False # Whether dead or not
+ self.model = pygame.image.load(config.jumping[0]).convert_alpha() # Model
+ self.tag = 'player' # Tag
+ self.direction = '' # Direction
+ self.condition = '' # Current condition
+
+ def _fix_velocity(self):
+ # Set velocity between minimum and maximum values
+ self._velocity.y = min(self._velocity.y, self.__maxvelocity.y)
+ self._velocity.y = round(max(self._velocity.y, -self.__maxvelocity.y), 2)
+ self._velocity.x = min(self._velocity.x, self.__maxvelocity.x)
+ self._velocity.x = round(max(self._velocity.x, -self.__maxvelocity.x), 2)
+
+ def reset(self):
+ # Reset the game
+ self._velocity = pygame.Vector2()
+ self.rect = self.__startrect.copy()
+ self.camera_rect = self.__startrect.copy()
+ self.dead = False
+
+ def handle_event(self, event: pygame.event.Event):
+ # Check for movement initiation
+ if event.type == pygame.KEYDOWN:
+ # Left-right movement
+ if event.key == pygame.K_a: # Left
+ self._velocity.x = -self.__startspeed
+ self._input = -1
+ self.left = True
+ self.right = False
+ self.direction = 'left'
+ elif event.key == pygame.K_d: # Right
+ self._velocity.x = self.__startspeed
+ self._input = 1
+ self.right = True
+ self.left = False
+ self.direction = 'right'
+
+ # Check for movement cessation
+ elif event.type == pygame.KEYUP:
+ if (event.key == pygame.K_a and self._input == -1) or (event.key == pygame.K_d and self._input == 1):
+ self._input = 0
+
+ def jump(self, force: float = None):
+ # Jump
+ if not force: # If force is None or False
+ force = self._jumpforce # Assign jump force
+ self._velocity.y = -force # Change the y-component of the velocity vector
+
+ def onCollide(self, obj: Sprite):
+ # Collision with the upper part of a platform
+ self.rect.bottom = obj.rect.top
+ self.jump()
+
+ def get_status(self):
+ # Get movement status
+ if 0.5 > self._velocity.y >= -21:
+ self.condition = 'jumping'
+ elif self._velocity.y < -21:
+ self.condition = 'bonus_effect'
+ else:
+ self.condition = 'falling'
+
+ def collisions(self):
+ # Collisions
+ lvl = Level.instance # Instance of the Level class
+ if not lvl: # If the instance doesn't exist, return None and stop execution
+ return
+ for platform in lvl.platforms: # Iterate through platforms
+ # Check for falling or collision
+ if self._velocity.y > 0.5: # Vertical speed is greater, indicating the object should fall
+ # Check for collision with a bonus
+ if platform.bonus and pygame.sprite.collide_rect(self, platform.bonus):
+ config.explosion.play() # Play explosion sound
+ self.onCollide(platform.bonus)
+ platform.bonus.effect = True
+ self.jump(platform.bonus.force) # Apply higher jump force
+ self.bonus_effect_start_time = pygame.time.get_ticks()
+ # Check for collision with a platform
+ if pygame.sprite.collide_rect(self, platform):
+ config.jump.play() # Play jump sound
+ self.onCollide(platform)
+ platform.onCollide()
+
+ def update(self):
+ # Player movement
+ if self.camera_rect.y > config.YWIN * 2:
+ self.dead = True
+ return
+ # Update velocity (applying gravity, input data)
+ self._velocity.y += self.gravity # Change velocity based on gravity
+ if self._input: # Accelerate along the x-axis when a key is pressed
+ self._velocity.x += self._input * self.accel
+ elif self._velocity.x: # Decelerate along the x-axis when no key is pressed
+ self._velocity.x -= getsign(self._velocity.x) * self.deccel
+ self._velocity.x = round(self._velocity.x)
+ self._fix_velocity()
+
+ # Update position
+ self.rect.x = (self.rect.x + self._velocity.x) % (config.XWIN - self.rect.width) # Update horizontal position
+ self.rect.y += self._velocity.y # Update vertical position
+
+ self.collisions()
diff --git a/requirements.txt b/requirements.txt
index ac7421d..dc22d56 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
-pygame==2.0.1
\ No newline at end of file
+Pygame 2.5.1
+Python 3.7.7
diff --git a/screenshots/preview_1.jpg b/screenshots/preview_1.jpg
new file mode 100644
index 0000000..d2737c4
Binary files /dev/null and b/screenshots/preview_1.jpg differ
diff --git a/screenshots/preview_2.jpg b/screenshots/preview_2.jpg
new file mode 100644
index 0000000..b009ad0
Binary files /dev/null and b/screenshots/preview_2.jpg differ
diff --git a/screenshots/preview_3.jpg b/screenshots/preview_3.jpg
new file mode 100644
index 0000000..b564f53
Binary files /dev/null and b/screenshots/preview_3.jpg differ
diff --git a/screenshots/preview_4.jpg b/screenshots/preview_4.jpg
new file mode 100644
index 0000000..5e758ac
Binary files /dev/null and b/screenshots/preview_4.jpg differ
diff --git a/settings.py b/settings.py
index 83a6768..cb57d12 100644
--- a/settings.py
+++ b/settings.py
@@ -1,41 +1,122 @@
-# -*- coding: utf-8 -*-
-from pygame.font import SysFont
-from pygame import init
-init()
-# ==================================
-
-#Window Settings
-XWIN, YWIN = 600,800 # Resolution
-HALF_XWIN,HALF_YWIN = XWIN/2,YWIN/2 # Center
-DISPLAY = (XWIN,YWIN)
-FLAGS = 0 # Fullscreen, resizeable...
-FPS = 60 # Render frame rate
+import pygame
+from button import Button
+pygame.init()
+
+# Video settings
+XWIN, YWIN = 800, 800 # Resolution
+HALF_XWIN, HALF_YWIN = XWIN / 2, YWIN / 2 # Screen center
+DISPLAY = (XWIN, YWIN) # Create screen instance
+FLAGS = pygame.DOUBLEBUF # Screen options: 0 - none, pygame.FULLSCREEN - fullscreen, pygame.DOUBLEBUF - double buffering, pygame.SCALED - scaling
+FPS = 60
+
+# Fonts
+pixel_font = 'graphics\\font\\font.ttf'
# Colors
-BLACK = (0,0,0)
-WHITE = (255,255,255)
-GRAY = (100,100,100)
-LIGHT_GREEN = (131,252,107)
-ANDROID_GREEN = (164,198,57)
-FOREST_GREEN = (87,189,68)
+BLACK = (0, 0, 0)
+WHITE = (255, 255, 255)
+GRAY = (100, 100, 100)
+LIGHT_GREEN = (131, 252, 107)
+ANDROID_GREEN = (164, 198, 57)
+FOREST_GREEN = (87, 189, 68)
+DARK_BLUE = (2, 3, 28)
# Player
-PLAYER_SIZE = (25,35)
-PLAYER_COLOR = ANDROID_GREEN
-PLAYER_MAX_SPEED = 20
-PLAYER_JUMPFORCE = 20
-PLAYER_BONUS_JUMPFORCE = 70
-GRAVITY = .98
-
-# Platforms
-PLATFORM_COLOR = FOREST_GREEN
-PLATFORM_COLOR_LIGHT = LIGHT_GREEN
-PLATFORM_SIZE = (100,10)
-PLATFORM_DISTANCE_GAP = (50,210)
-MAX_PLATFORM_NUMBER = 10
-BONUS_SPAWN_CHANCE = 10
-BREAKABLE_PLATFORM_CHANCE = 12
+PLAYER_SIZE = (90, 60) # Size: width, height
+PLAYER_MAX_SPEED = 20 # Maximum speed
+PLAYER_JUMPFORCE = 20 # Maximum acceleration
+PLAYER_BONUS_JUMPFORCE = 70 # Maximum bonus acceleration
+GRAVITY = 0.92 # Gravity
-# Fonts
-LARGE_FONT = SysFont("",128)
-SMALL_FONT = SysFont("arial",24)
\ No newline at end of file
+# Platform
+PLATFORM_SIZE = (110, 10) # Platform size
+PLATFORM_DISTANCE_GAP = (50, 130) # Distance between platforms
+MAX_PLATFORM_NUMBER = 12 # Maximum number of platforms for generation
+BONUS_WIDTH = 35 # Bonus width
+BONUS_HEIGHT = 35 # Bonus height
+BONUS_SPAWN_CHANCE = 6 # Bonus spawn chance
+BREAKABLE_PLATFORM_CHANCE = 6 # Breakable platform spawn chance
+
+# Music
+music_volume = 0.2
+level_music = 'audio\\level_music.wav'
+overworld_music = 'audio\\overworld_music.wav'
+
+# Sounds
+sound_volume = 1
+activate_button = 'audio\\effects\\activate_button.ogg'
+check_button = 'audio\\effects\\check_button.ogg'
+jump = 'audio\\effects\\jump.ogg'
+stomp = 'audio\\effects\\stomp.ogg'
+explosion = 'audio\\effects\\explosion.ogg'
+game_over = 'audio\\effects\\game_over.ogg'
+
+activate_button = pygame.mixer.Sound(activate_button)
+activate_button.set_volume(1)
+
+check_button = pygame.mixer.Sound(check_button)
+check_button.set_volume(1)
+
+jump = pygame.mixer.Sound(jump)
+jump.set_volume(0.1)
+
+stomp = pygame.mixer.Sound(stomp)
+stomp.set_volume(1)
+
+explosion = pygame.mixer.Sound(explosion)
+explosion.set_volume(1)
+
+game_over = pygame.mixer.Sound(game_over)
+game_over.set_volume(0.5)
+
+# Animation
+jumping = [ # Jumping animation
+ 'graphics\\character\\jump\\1.png',
+]
+
+falling = [ # Falling animation
+ 'graphics\\character\\fall\\2.png',
+ 'graphics\\character\\fall\\3.png',
+ 'graphics\\character\\fall\\4.png',
+ 'graphics\\character\\fall\\5.png',
+]
+
+bonus_explosion = [ # Bonus explosion animation
+ 'graphics\\bonus\\explosion\\1.png',
+ 'graphics\\bonus\\explosion\\2.png',
+ 'graphics\\bonus\\explosion\\3.png',
+ 'graphics\\bonus\\explosion\\4.png',
+ 'graphics\\bonus\\explosion\\5.png',
+ 'graphics\\bonus\\explosion\\6.png',
+]
+
+# Images
+# For gameplay
+background_image = 'graphics\\background\\background.png'
+bonus_default_image = 'graphics\\bonus\\defaultsituation\\power_keg_with_m.png'
+platform_image = 'graphics\\platform\\normal\\platform.png'
+broken_platform_image = 'graphics\\platform\\broken\\broken-platform.png'
+
+# For the main menu
+main_menu_background = 'graphics\\main_menu\\background\\background.png'
+exit_button_non_active = 'graphics\\main_menu\\button\\non_active\\exit_btn.png'
+start_button_non_active = 'graphics\\main_menu\\button\\non_active\\start_btn.png'
+exit_button_active = 'graphics\\main_menu\\button\\active\\exit_btn.png'
+start_button_active = 'graphics\\main_menu\\button\\active\\start_btn.png'
+marwin_image = 'graphics\\main_menu\\marwin.png'
+
+# For the game over and pause menus
+game_over_image = 'graphics\\game_over_menu\\game_over.png'
+restart_button_non_active = 'graphics\\game_over_menu\\buttons\\non_active\\restart.png'
+mainmenu_button_non_active = 'graphics\\game_over_menu\\buttons\\non_active\\main_menu.png'
+resume_button_non_active = 'graphics\\pause\\buttons\\non_active\\resume.png'
+restart_button_active = 'graphics\\game_over_menu\\buttons\\active\\restart.png'
+mainmenu_button_active = 'graphics\\game_over_menu\\buttons\\active\\main_menu.png'
+resume_button_active = 'graphics\\pause\\buttons\\active\\resume.png'
+
+# UI
+frame_image_score = 'graphics\\UI\\frame.png'
+frame_image_pause = 'graphics\\pause\\frame.png'
+
+# Icon
+icon = 'graphics\\UI\\m.png'
diff --git a/singleton.py b/singleton.py
index 9f08729..81dcccb 100644
--- a/singleton.py
+++ b/singleton.py
@@ -1,32 +1,12 @@
-# -*- coding: utf-8 -*-
-"""
- CopyLeft 2021 Michael Rouves
-
- This file is part of Pygame-DoodleJump.
- Pygame-DoodleJump is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Pygame-DoodleJump is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Pygame-DoodleJump. If not, see .
-"""
-
-
-
+# Singleton allows creating only one instance of the class and provides a global access point to this instance,
+# to avoid AttributeError
class Singleton:
- """
- Singleton pattern.
- Overload only class that must have one instance.
- Stores the instance in a static variable: Class.instance
- (Check Singleton design pattern for more info)
- """
- def __new__(cls,*args,**kwargs):
- if not hasattr(cls, 'instance'):
- cls.instance = super(Singleton, cls).__new__(cls)
- return cls.instance
+ def __new__(cls, *args, **kwargs):
+ # Handles the process of creating an instance of the class
+ if not hasattr(cls, 'instance'):
+ # Check if the 'instance' attribute exists in the class 'cls'.
+ # If it doesn't exist, it means an instance of the class hasn't been created yet.
+ cls.instance = super(Singleton, cls).__new__(cls)
+ # Create a new instance of the class
+ return cls.instance
+ # Returns the instance of the class
diff --git a/sprite.py b/sprite.py
index 3ea9f93..7fd7f19 100644
--- a/sprite.py
+++ b/sprite.py
@@ -1,68 +1,57 @@
-# -*- coding: utf-8 -*-
-"""
- CopyLeft 2021 Michael Rouves
-
- This file is part of Pygame-DoodleJump.
- Pygame-DoodleJump is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Pygame-DoodleJump is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Pygame-DoodleJump. If not, see .
-"""
-
-
-from pygame import Surface,Rect
+import settings as config
from camera import Camera
-
-
+import pygame
class Sprite:
- """
- A class to represent a sprite.
-
- Used for pygame displaying.
- Image generated with given color and size.
- """
- # default constructor (must be called if overrided by inheritance)
- def __init__(self,x:int,y:int,w:int,h:int,color:tuple):
- self.__color = color
- self._image = Surface((w,h))
- self._image.fill(self.color)
- self._image = self._image.convert()
- self.rect = Rect(x,y,w,h)
- self.camera_rect = self.rect.copy()
-
- # Public getters for _image & __color so they remain private
- @property
- def image(self) -> Surface:
- return self._image
- @property
- def color(self) -> tuple:
- return self.__color
-
- @color.setter
- def color(self, new:tuple) -> None:
- " Called when Sprite.__setattr__('color',x)."
- assert isinstance(new,tuple) and len(new)==3,"Value is not a color"
- self.__color = new
- #update image surface
- self._image.fill(self.color)
-
-
- def draw(self, surface:Surface) -> None:
- """ Render method,Should be called every frame after update.
- :param surface pygame.Surface: the surface to draw on.
- """
- # If camera instancied: calculate render positon
- if Camera.instance:
- self.camera_rect = Camera.instance.apply(self)
- surface.blit(self._image,self.camera_rect)
- else:
- surface.blit(self._image,self.rect)
\ No newline at end of file
+ # Constructor for the sprite model
+ def __init__(self, x: int, y: int, w: int, h: int):
+ self.width = w
+ self.height = h
+ self.rect = pygame.Rect(x, y, w, h) # Create a rectangle to define the sprite's position on the screen
+ self.camera_rect = self.rect.copy() # Track the sprite's position for the camera
+
+ def draw(self, surface: pygame.Surface):
+ # Draw function
+ if Camera.instance: # If a camera instance is created
+ self.camera_rect = Camera.instance.apply(self) # Apply the rect of the current instance
+
+ if self.tag == 'player': # If it's the player
+ self.get_status() # Get the status
+ if self.condition == 'jumping':
+ self.model = pygame.transform.scale(pygame.image.load(config.jumping[0]).convert_alpha(), (config.PLAYER_SIZE[0], config.PLAYER_SIZE[1])) # Jumping model
+ elif self.condition == 'falling':
+ self.model = pygame.transform.scale(pygame.image.load(config.falling[2]).convert_alpha(), (config.PLAYER_SIZE[0], config.PLAYER_SIZE[1])) # Falling model
+ elif self.condition == 'bonus_effect':
+ self.model = pygame.transform.scale(pygame.image.load(config.falling[3]).convert_alpha(), (config.PLAYER_SIZE[0], config.PLAYER_SIZE[1])) # Bonus effect model
+
+ if self.direction == 'left': # Flip the image when moving in different directions
+ self.model = pygame.transform.flip(self.model, True, False)
+
+ surface.blit(self.model, self.camera_rect) # Draw with the applied model
+
+ if self.tag == 'bonus': # If it's a bonus
+ if not self.effect: # If not activated
+ self.model = pygame.transform.scale(pygame.image.load(config.bonus_default_image).convert_alpha(), (self.width, self.height)) # Default image
+ else:
+ if not self.animation_finished: # If animation is not finished
+ if self.bonus_effect_start_time is None: # Assign animation start time
+ self.bonus_effect_start_time = pygame.time.get_ticks()
+
+ current_time = pygame.time.get_ticks() # Total time
+ elapsed_time = current_time - self.bonus_effect_start_time # Calculate remaining animation time
+
+ if elapsed_time >= self.bonus_effect_duration: # If remaining time is greater than animation effect duration in ms
+ self.animation_finished = True # Finish animation
+
+ # Determine current animation frame based on elapsed time and frame duration
+ self.current_frame = int(elapsed_time / (self.bonus_effect_duration / len(config.bonus_explosion)))
+ if self.current_frame >= len(config.bonus_explosion):
+ self.current_frame = len(config.bonus_explosion) - 1
+
+ self.model = pygame.image.load(config.bonus_explosion[self.current_frame]).convert_alpha() # Apply image to model
+ surface.blit(self.model, self.camera_rect) # Draw
+
+ if self.tag == 'platform': # If it's a platform
+ surface.blit(pygame.transform.scale(self.model, (self.width, self.height)), self.camera_rect) # Draw the specified model
+ else:
+ surface.blit(self.model, self.rect) # If no camera instance is created yet, draw the instance itself first