Merge pull request #13 from BreizhHardware/dev_felix

Refactor(Game) - Introduce GameResources class to centralize game con…
This commit is contained in:
Félix MARQUET
2025-03-28 10:21:12 +01:00
committed by GitHub
11 changed files with 159 additions and 134 deletions

45
main.py
View File

@@ -4,17 +4,7 @@ from pygame.locals import *
from src.Entity.Enemy import Enemy
from src.game import initialize_game, reset_game, reset_game_with_checkpoint
from src.constant import (
displaysurface,
FramePerSec,
font,
FPS,
WIDTH,
HEIGHT,
ORIGINAL_WIDTH,
ORIGINAL_HEIGHT,
fullscreen,
)
from src.constant import GameResources
from src.Menu.Menu import Menu
from src.Menu.Leaderboard import Leaderboard
from src.Camera import Camera
@@ -22,11 +12,20 @@ from src.Database.CheckpointDB import CheckpointDB
def main():
# Declare globals that we'll modify
global displaysurface, fullscreen, ORIGINAL_WIDTH, ORIGINAL_HEIGHT
# Initialize Pygame and game resources
game_resources = GameResources()
displaysurface = game_resources.displaysurface
FramePerSec = game_resources.FramePerSec
font = game_resources.font
FPS = game_resources.FPS
WIDTH = game_resources.WIDTH
HEIGHT = game_resources.HEIGHT
ORIGINAL_WIDTH = game_resources.ORIGINAL_WIDTH
ORIGINAL_HEIGHT = game_resources.ORIGINAL_HEIGHT
fullscreen = game_resources.fullscreen
# Add camera initialization
camera = Camera(WIDTH, HEIGHT)
camera = Camera(WIDTH, HEIGHT, game_resources)
# Game states
MENU = 0
@@ -36,12 +35,12 @@ def main():
# Initialize game state and objects
current_state = MENU
menu = Menu()
leaderboard = Leaderboard()
menu = Menu(game_resources)
leaderboard = Leaderboard(WIDTH, HEIGHT, font)
# Initialize game components
P1, PT1, platforms, all_sprites, background, checkpoints = initialize_game(
"map_test.json"
game_resources, "map_test.json"
)
projectiles = pygame.sprite.Group()
@@ -88,14 +87,12 @@ def main():
if checkpoint_pos:
# Respawn player at checkpoint
P1, platforms, all_sprites, background, checkpoints = (
reset_game_with_checkpoint("map_test.json")
reset_game_with_checkpoint("map_test.json", game_resources)
)
projectiles.empty()
print("Joueur réanimé au checkpoint")
else:
# No checkpoint found, return to menu
current_state = MENU
print("Game over - retour au menu")
if event.dict.get("action") == "create_projectile":
projectile = event.dict.get("projectile")
projectiles.add(projectile)
@@ -104,7 +101,9 @@ def main():
if current_state == MENU:
action = menu.handle_event(event)
if action == "play":
P1, platforms, all_sprites, background, checkpoints = reset_game()
P1, platforms, all_sprites, background, checkpoints = reset_game(
game_resources
)
current_state = PLAYING
elif action == "infinite":
current_state = INFINITE
@@ -185,10 +184,6 @@ def main():
camera_adjusted_rect.y += camera.camera.y
displaysurface.blit(projectile.surf, camera_adjusted_rect)
print(
f"Projectile: pos={projectile.pos}, rect={projectile.rect}, camera={camera.camera}"
)
checkpoints_hit = pygame.sprite.spritecollide(P1, checkpoints, False)
for checkpoint in checkpoints_hit:
checkpoint.activate()

View File

@@ -1,18 +1,18 @@
# src/Camera.py
import pygame
from src.constant import WIDTH, HEIGHT
class Camera:
def __init__(self, width, height):
def __init__(self, width, height, game_resources):
self.camera = pygame.Rect(0, 0, width, height)
self.width = width
self.height = height
self.game_resources = game_resources
def update(self, target):
# Center the target in the camera view
x = -target.rect.centerx + WIDTH // 2
y = -target.rect.centery + HEIGHT // 2
x = -target.rect.centerx + self.game_resources.WIDTH // 2
y = -target.rect.centery + self.game_resources.HEIGHT // 2
# Update camera position
self.camera = pygame.Rect(x, y, self.width, self.height)

View File

@@ -43,6 +43,5 @@ class Checkpoint(Entity):
# Save checkpoint to database
self.db.save_checkpoint(self.map_name, self.pos.x, self.pos.y)
print("checkpoint")
return True
return False

View File

@@ -1,6 +1,5 @@
import pygame
from src.Entity.Entity import Entity
from src.constant import FPS
from pygame.math import Vector2 as vec
from src.Entity.Projectile import Projectile
@@ -98,7 +97,7 @@ class Enemy(Entity):
else:
self.detected_player = False
def stationary_attack(self, player):
def stationary_attack(self, player, FPS=60):
"""Remote attack for turret-like enemies"""
distance_to_player = vec(
player.pos.x - self.pos.x, player.pos.y - self.pos.y
@@ -140,7 +139,6 @@ class Enemy(Entity):
self.health -= amount
if self.health <= 0:
self.kill()
print("Enemy killed!")
return True
return False

View File

@@ -1,14 +1,16 @@
from src.Entity.Entity import Entity
from src.constant import WIDTH, vec, ACC, FRIC, platforms, FPS, life_icon_width
from pygame import *
import pygame
import os
class Player(Entity):
def __init__(self, width=100, height=100, x=10, y=385):
def __init__(self, game_resources, width=100, height=100, x=10, y=385):
super().__init__(pos=(x, y), size=(width, height), color=(128, 255, 40))
# Game ressources
self.game_resources = game_resources
# Animation variables
self.animation_frames = []
self.jump_frames = []
@@ -106,11 +108,20 @@ class Player(Entity):
"assets/player/Sanic Head.png"
).convert_alpha()
self.life_icon = pygame.transform.scale(
self.life_icon, (life_icon_width, life_icon_width)
self.life_icon,
(
self.game_resources.life_icon_width,
self.game_resources.life_icon_width,
),
)
else:
# Backup: use a red square
self.life_icon = pygame.Surface((life_icon_width, life_icon_width))
self.life_icon = pygame.Surface(
(
self.game_resources.life_icon_width,
self.game_resources.life_icon_width,
)
)
self.life_icon.fill((255, 0, 0))
except Exception as e:
@@ -155,7 +166,7 @@ class Player(Entity):
if self.dashing and current_time - self.dash_start_time >= self.dash_duration:
self.dashing = False
self.acc = vec(0, 1) # Gravity
self.acc = self.game_resources.vec(0, 1)
# Reset flags
self.moving = False
@@ -164,15 +175,15 @@ class Player(Entity):
if pressed_keys[K_q]:
# Check if X is > 0 to prevent player from going off screen
if self.pos.x > 0:
self.acc.x = -ACC
self.acc.x = -self.game_resources.ACC
self.moving = True
if pressed_keys[K_a]:
self.dash(-ACC)
self.dash(-self.game_resources.ACC)
if pressed_keys[K_d]:
self.acc.x = ACC
self.acc.x = self.game_resources.ACC
self.moving = True
if pressed_keys[K_a]:
self.dash(ACC)
self.dash(self.game_resources.ACC)
# Also consider the player moving if they have significant horizontal velocity
if abs(self.vel.x) > 0.5:
@@ -184,8 +195,8 @@ class Player(Entity):
self.jumping = True
# Apply friction
self.acc.y += self.vel.y * FRIC
self.acc.x += self.vel.x * FRIC
self.acc.y += self.vel.y * self.game_resources.FRIC
self.acc.x += self.vel.x * self.game_resources.FRIC
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
@@ -215,7 +226,7 @@ class Player(Entity):
)
def update(self):
hits = pygame.sprite.spritecollide(self, platforms, False)
hits = pygame.sprite.spritecollide(self, self.game_resources.platforms, False)
if hits:
if self.vel.y > 0:
self.pos.y = hits[0].rect.top
@@ -223,7 +234,7 @@ class Player(Entity):
self.jumping = False
if self.invulnerable:
self.invulnerable_timer += 1 / FPS
self.invulnerable_timer += 1 / self.game_resources.FPS
if self.invulnerable_timer >= self.invulnerable_duration:
self.invulnerable = False
self.invulnerable_timer = 0
@@ -249,14 +260,20 @@ class Player(Entity):
def draw_lives(self, surface):
"""Draws the player's remaining lives as icons in the top right corner."""
spacing = 5
start_x = surface.get_width() - (self.max_lives * (life_icon_width + spacing))
start_x = surface.get_width() - (
self.max_lives * (self.game_resources.life_icon_width + spacing)
)
start_y = 10
for i in range(self.max_lives):
if i < self.lives:
# Vie active: afficher l'icône normale
surface.blit(
self.life_icon, (start_x + i * (life_icon_width + spacing), start_y)
self.life_icon,
(
start_x + i * (self.game_resources.life_icon_width + spacing),
start_y,
),
)
else:
# Vie perdue: afficher l'icône grisée
@@ -268,5 +285,9 @@ class Player(Entity):
gray = (color[0] + color[1] + color[2]) // 3
grayscale_icon.set_at((x, y), (gray, gray, gray, color[3]))
surface.blit(
grayscale_icon, (start_x + i * (life_icon_width + spacing), start_y)
grayscale_icon,
(
start_x + i * (self.game_resources.life_icon_width + spacing),
start_y,
),
)

View File

@@ -5,13 +5,13 @@ from src.Entity.Platform import Platform
from src.Entity.Player import Player
from src.Entity.Enemy import Enemy
from src.Entity.Checkpoint import Checkpoint
from src.constant import WIDTH, HEIGHT, all_sprites, platforms
class MapParser:
def __init__(self):
self.all_sprites = all_sprites
self.platforms = platforms
def __init__(self, game_resources):
self.game_resources = game_resources
self.all_sprites = self.game_resources.all_sprites
self.platforms = self.game_resources.platforms
self.enemies = pygame.sprite.Group()
self.collectibles = pygame.sprite.Group()
self.checkpoints = pygame.sprite.Group()
@@ -34,8 +34,8 @@ class MapParser:
"collectibles": self.collectibles,
"map_properties": {
"name": map_data.get("name", "Unnamed Level"),
"width": map_data.get("width", WIDTH),
"height": map_data.get("height", HEIGHT),
"width": map_data.get("width", self.game_resources.WIDTH),
"height": map_data.get("height", self.game_resources.HEIGHT),
},
"checkpoints": self.checkpoints,
}
@@ -105,7 +105,7 @@ class MapParser:
# Create player at spawn point
spawn = map_data.get("spawn_point", {"x": 50, "y": 700})
self.player = Player()
self.player = Player(self.game_resources)
self.player.pos.x = spawn["x"]
self.player.pos.y = spawn["y"]
self.all_sprites.add(self.player)
@@ -125,12 +125,12 @@ class MapParser:
# Create background image
if "background" in map_data:
print(f"Loading background image: {map_data['background']}")
if os.path.isfile(map_data["background"]):
background = pygame.image.load(map_data["background"]).convert_alpha()
background = pygame.transform.scale(background, (WIDTH, HEIGHT))
background = pygame.transform.scale(
background, (self.game_resources.WIDTH, self.game_resources.HEIGHT)
)
self.background = background
print("Background image loaded")
else:
print(f"Background image not found: {map_data['background']}")
else:

View File

@@ -1,6 +1,5 @@
from pygame.locals import *
import pygame
from src.constant import font
class Button:
@@ -13,7 +12,7 @@ class Button:
self.action = action
self.hover = False
def draw(self, surface):
def draw(self, surface, font):
# Button colors
color = (100, 149, 237) if self.hover else (65, 105, 225)
border_color = (255, 255, 255)

View File

@@ -1,21 +1,25 @@
import pygame
from src.constant import WIDTH, HEIGHT, font
from src.Menu.Button import Button
class Leaderboard:
def __init__(self):
def __init__(self, HEIGHT, WIDTH, font):
self.HEIGHT = HEIGHT
self.WIDTH = WIDTH
self.font = font
self.tabs = ["Mode Normal", "Mode Infini"]
self.current_tab = 0
self.scores = {
0: [("Player1", 1000), ("Player2", 800), ("Player3", 600)],
1: [("Player1", 2000), ("Player2", 1500), ("Player3", 1200)],
}
self.back_button = Button("Retour", 20, HEIGHT - 70, 120, 50, "menu")
self.back_button = Button("Retour", 20, self.HEIGHT - 70, 120, 50, "menu")
tab_width = 150
self.tab_buttons = [
Button(self.tabs[0], WIDTH // 2 - tab_width, 80, tab_width, 40, "tab_0"),
Button(self.tabs[1], WIDTH // 2, 80, tab_width, 40, "tab_1"),
Button(
self.tabs[0], self.WIDTH // 2 - tab_width, 80, tab_width, 40, "tab_0"
),
Button(self.tabs[1], self.WIDTH // 2, 80, tab_width, 40, "tab_1"),
]
def draw(self, surface):
@@ -23,7 +27,7 @@ class Leaderboard:
title = pygame.font.SysFont("Arial", 48).render(
"Classement", True, (0, 191, 255)
)
title_rect = title.get_rect(center=(WIDTH // 2, 40))
title_rect = title.get_rect(center=(self.WIDTH // 2, 40))
surface.blit(title, title_rect)
# Draw tabs
@@ -39,8 +43,12 @@ class Leaderboard:
# Draw scores
y_pos = 150
for i, (name, score) in enumerate(self.scores[self.current_tab]):
rank_text = font.render(f"{i+1}. {name}: {score}", True, (255, 255, 255))
surface.blit(rank_text, (WIDTH // 2 - rank_text.get_width() // 2, y_pos))
rank_text = self.font.render(
f"{i+1}. {name}: {score}", True, (255, 255, 255)
)
surface.blit(
rank_text, (self.WIDTH // 2 - rank_text.get_width() // 2, y_pos)
)
y_pos += 40
self.back_button.draw(surface)

View File

@@ -1,21 +1,21 @@
import pygame
from src.constant import HEIGHT, WIDTH
from src.Menu.Button import Button
class Menu:
def __init__(self):
def __init__(self, game_resources):
self.game_resources = game_resources
self.buttons = []
button_width = 250
button_height = 60
button_spacing = 20
start_y = HEIGHT // 2 - 100
start_y = self.game_resources.HEIGHT // 2 - 100
# Create buttons centered horizontally
self.buttons.append(
Button(
"Jouer",
WIDTH // 2 - button_width // 2,
self.game_resources.WIDTH // 2 - button_width // 2,
start_y,
button_width,
button_height,
@@ -27,7 +27,7 @@ class Menu:
self.buttons.append(
Button(
"Jouer en mode infini",
WIDTH // 2 - button_width // 2,
self.game_resources.WIDTH // 2 - button_width // 2,
start_y,
button_width,
button_height,
@@ -39,7 +39,7 @@ class Menu:
self.buttons.append(
Button(
"Classement",
WIDTH // 2 - button_width // 2,
self.game_resources.WIDTH // 2 - button_width // 2,
start_y,
button_width,
button_height,
@@ -51,7 +51,7 @@ class Menu:
self.buttons.append(
Button(
"Quitter",
WIDTH // 2 - button_width // 2,
self.game_resources.WIDTH // 2 - button_width // 2,
start_y,
button_width,
button_height,
@@ -64,12 +64,14 @@ class Menu:
title = pygame.font.SysFont("Arial", 72).render(
"Project Sanic", True, (0, 191, 255)
)
title_rect = title.get_rect(center=(WIDTH // 2, HEIGHT // 4))
title_rect = title.get_rect(
center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT // 4)
)
surface.blit(title, title_rect)
# Draw buttons
for button in self.buttons:
button.draw(surface)
button.draw(surface, self.game_resources.font)
def handle_event(self, event):
for button in self.buttons:

View File

@@ -1,24 +1,33 @@
import pygame
pygame.init()
FPS = 60
ACC = 0.5
FRIC = -0.12
WIDTH = 1200
HEIGHT = 800
platforms = pygame.sprite.Group()
vec = pygame.math.Vector2
displaysurface = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE)
pygame.display.set_caption("Project Sanic")
FramePerSec = pygame.time.Clock()
all_sprites = pygame.sprite.Group()
fullscreen = False
ORIGINAL_WIDTH = WIDTH
ORIGINAL_HEIGHT = HEIGHT
life_icon_width = 50
class GameResources:
def __init__(self):
pygame.init()
try:
font = pygame.font.SysFont("Arial", 20)
except:
font = pygame.font.Font(None, 20)
# Constantes
self.FPS = 60
self.ACC = 0.5
self.FRIC = -0.12
self.WIDTH = 1200
self.HEIGHT = 800
self.ORIGINAL_WIDTH = self.WIDTH
self.ORIGINAL_HEIGHT = self.HEIGHT
self.life_icon_width = 50
self.fullscreen = False
# Ressources
self.platforms = pygame.sprite.Group()
self.vec = pygame.math.Vector2
self.displaysurface = pygame.display.set_mode(
(self.WIDTH, self.HEIGHT), pygame.RESIZABLE
)
pygame.display.set_caption("Project Sanic")
self.FramePerSec = pygame.time.Clock()
self.all_sprites = pygame.sprite.Group()
# Font
try:
self.font = pygame.font.SysFont("Arial", 20)
except:
self.font = pygame.font.Font(None, 20)

View File

@@ -3,83 +3,77 @@ import sys
from pygame.locals import *
from src.Entity.Platform import Platform
from src.Entity.Player import Player
from src.constant import (
displaysurface,
FramePerSec,
font,
FPS,
platforms,
all_sprites,
vec,
)
from src.Map.parser import MapParser
from src.Database.CheckpointDB import CheckpointDB
def initialize_game(map_file="map_test.json"):
def initialize_game(game_resources, map_file="map_test.json"):
"""Initialize game with map from JSON file"""
parser = MapParser()
parser = MapParser(game_resources)
map_objects = parser.load_map(map_file)
if not map_objects:
# Fallback to default setup if map loading fails
platforms.empty()
all_sprites.empty()
game_resources.platforms.empty()
game_resources.all_sprites.empty()
PT1 = Platform(1200, 20, 600, 400)
P1 = Player()
P1 = Player(game_resources)
platforms.add(PT1)
all_sprites.add(PT1)
all_sprites.add(P1)
game_resources.platforms.add(PT1)
game_resources.all_sprites.add(PT1)
game_resources.all_sprites.add(P1)
return P1, PT1, platforms, all_sprites, None # Return None for background
return (
P1,
PT1,
game_resources.platforms,
game_resources.all_sprites,
None,
None,
)
return (
map_objects["player"],
None, # No specific platform reference needed
None,
map_objects["platforms"],
map_objects["all_sprites"],
parser.background, # Return the loaded background
parser.background,
map_objects["checkpoints"],
)
def reset_game():
def reset_game(game_resources):
"""Reset the game to initial state"""
global platforms, all_sprites, camera
# Empty all sprite groups
platforms.empty()
all_sprites.empty()
# Reload game objects
player, _, platforms, all_sprites, background, checkpoints = initialize_game(
"map_test.json"
game_resources, "map_test.json"
)
return player, platforms, all_sprites, background, checkpoints
def reset_game_with_checkpoint(map_name="map_test.json"):
def reset_game_with_checkpoint(map_name, game_resources):
"""
Reset the game and respawn player at checkpoint if available
Args:
map_name: Name of the current map
game_resources: GameResources object
"""
# Initialize game normally
player, platforms, all_sprites, background, checkpoints = reset_game()
# Check if there's a saved checkpoint
# Check the checkpoint database for saved checkpoint
db = CheckpointDB()
checkpoint_pos = db.get_checkpoint(map_name)
# If checkpoint exists, teleport player there
# Initialize game
player, _, platforms, all_sprites, background, checkpoints = initialize_game(
game_resources, map_name
)
# If checkpoint exists, respawn player at checkpoint
if checkpoint_pos:
player.pos = vec(checkpoint_pos[0], checkpoint_pos[1])
player.pos = game_resources.vec(checkpoint_pos[0], checkpoint_pos[1])
player.update_rect()
print(f"Player respawned at checkpoint: {checkpoint_pos}")
return player, platforms, all_sprites, background, checkpoints