mirror of
https://github.com/BreizhHardware/project_sanic.git
synced 2026-03-18 21:50:33 +01:00
Feat(Levels) - Add level selection menu and database support for level progression; implement level unlocking and navigation between levels
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,4 +5,5 @@
|
||||
.idea/*
|
||||
|
||||
checkpoint.db
|
||||
checkpoint.db-journal
|
||||
checkpoint.db-journal
|
||||
game.db
|
||||
86
main.py
86
main.py
@@ -1,11 +1,14 @@
|
||||
import re
|
||||
|
||||
import pygame
|
||||
import sys
|
||||
from pygame.locals import *
|
||||
|
||||
from src.Database.LevelDB import LevelDB
|
||||
from src.Entity.Enemy import Enemy
|
||||
from src.Menu.LevelSelectMenu import LevelSelectMenu
|
||||
from src.game import (
|
||||
initialize_game,
|
||||
reset_game,
|
||||
reset_game_with_checkpoint,
|
||||
clear_checkpoint_database,
|
||||
)
|
||||
@@ -40,15 +43,13 @@ def main():
|
||||
|
||||
# Initialize game state and objects
|
||||
current_state = MENU
|
||||
menu = Menu(game_resources)
|
||||
main_menu = Menu(game_resources)
|
||||
level_select_menu = None
|
||||
level_file = "map/levels/1.json"
|
||||
current_menu = "main"
|
||||
leaderboard = Leaderboard(WIDTH, HEIGHT, font)
|
||||
|
||||
clear_checkpoint_database()
|
||||
|
||||
# Initialize game components
|
||||
P1, PT1, platforms, all_sprites, background, checkpoints, exits = initialize_game(
|
||||
game_resources, "map_test.json"
|
||||
)
|
||||
projectiles = pygame.sprite.Group()
|
||||
|
||||
# Main game loop
|
||||
@@ -89,12 +90,12 @@ def main():
|
||||
elif event.type == USEREVENT:
|
||||
if event.action == "player_death":
|
||||
db = CheckpointDB()
|
||||
checkpoint_pos = db.get_checkpoint("map_test.json")
|
||||
checkpoint_pos = db.get_checkpoint(level_file)
|
||||
|
||||
if checkpoint_pos:
|
||||
# Respawn player at checkpoint
|
||||
P1, platforms, all_sprites, background, checkpoints = (
|
||||
reset_game_with_checkpoint("map_test.json", game_resources)
|
||||
reset_game_with_checkpoint(level_file, game_resources)
|
||||
)
|
||||
projectiles.empty()
|
||||
else:
|
||||
@@ -106,19 +107,38 @@ def main():
|
||||
|
||||
# Handle menu events
|
||||
if current_state == MENU:
|
||||
action = menu.handle_event(event)
|
||||
if action == "play":
|
||||
P1, platforms, all_sprites, background, checkpoints = reset_game(
|
||||
game_resources
|
||||
)
|
||||
current_state = PLAYING
|
||||
elif action == "infinite":
|
||||
current_state = INFINITE
|
||||
elif action == "leaderboard":
|
||||
current_state = LEADERBOARD
|
||||
elif action == "quit":
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
if current_menu == "main":
|
||||
action = main_menu.handle_event(event)
|
||||
if action == "level_select":
|
||||
level_select_menu = LevelSelectMenu(game_resources)
|
||||
current_menu = "level_select"
|
||||
elif action == "infinite":
|
||||
current_state = INFINITE
|
||||
elif action == "leaderboard":
|
||||
current_state = LEADERBOARD
|
||||
elif action == "quit":
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
elif current_menu == "level_select":
|
||||
action = level_select_menu.handle_event(event)
|
||||
if action == "back_to_main":
|
||||
current_menu = "main"
|
||||
elif (
|
||||
isinstance(action, dict)
|
||||
and action.get("action") == "select_level"
|
||||
):
|
||||
level_file = action.get("level_file")
|
||||
(
|
||||
P1,
|
||||
PT1,
|
||||
platforms,
|
||||
all_sprites,
|
||||
background,
|
||||
checkpoints,
|
||||
exits,
|
||||
) = initialize_game(game_resources, level_file)
|
||||
projectiles.empty()
|
||||
current_state = PLAYING
|
||||
|
||||
# Handle leaderboard events
|
||||
elif current_state == LEADERBOARD:
|
||||
@@ -131,7 +151,10 @@ def main():
|
||||
|
||||
# Draw appropriate screen based on state
|
||||
if current_state == MENU:
|
||||
menu.draw(displaysurface)
|
||||
if current_menu == "main":
|
||||
main_menu.draw(displaysurface)
|
||||
elif current_menu == "level_select":
|
||||
level_select_menu.draw(displaysurface)
|
||||
|
||||
elif current_state == PLAYING:
|
||||
# Regular game code
|
||||
@@ -195,6 +218,23 @@ def main():
|
||||
for checkpoint in checkpoints_hit:
|
||||
checkpoint.activate()
|
||||
|
||||
exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else []
|
||||
for exit in exits_hit:
|
||||
current_level_match = re.search(r"(\d+)\.json$", level_file)
|
||||
if current_level_match:
|
||||
current_level = int(current_level_match.group(1))
|
||||
next_level = current_level + 1
|
||||
|
||||
# Unlock next level
|
||||
db = LevelDB()
|
||||
db.unlock_level(next_level)
|
||||
db.close()
|
||||
|
||||
# Return to level select menu
|
||||
current_state = MENU
|
||||
current_menu = "level_select"
|
||||
level_select_menu = LevelSelectMenu(game_resources)
|
||||
|
||||
# Display FPS and coordinates (fixed position UI elements)
|
||||
fps = int(FramePerSec.get_fps())
|
||||
fps_text = font.render(f"FPS: {fps}", True, (255, 255, 255))
|
||||
|
||||
186
map/levels/1.json
Normal file
186
map/levels/1.json
Normal file
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"name": "Level 1",
|
||||
"width": 2400,
|
||||
"height": 800,
|
||||
"background": "assets/map/background/forest_bg.jpg",
|
||||
"gravity": 1.0,
|
||||
|
||||
"ground": [
|
||||
{
|
||||
"id": "main_ground",
|
||||
"x": -1000,
|
||||
"y": 780,
|
||||
"width": 1800,
|
||||
"height": 200,
|
||||
"texture": "assets/map/platform/grass_texture.jpg"
|
||||
},
|
||||
{
|
||||
"id": "pit",
|
||||
"x": 800,
|
||||
"y": 780,
|
||||
"width": 200,
|
||||
"height": 20,
|
||||
"is_hole": true
|
||||
},
|
||||
{
|
||||
"id": "main_ground_2",
|
||||
"x": 1000,
|
||||
"y": 900,
|
||||
"width": 1800,
|
||||
"height": 200,
|
||||
"texture": "assets/map/platform/grass_texture.jpg"
|
||||
}
|
||||
],
|
||||
|
||||
"platforms": [
|
||||
{
|
||||
"id": "platform1",
|
||||
"x": 300,
|
||||
"y": 600,
|
||||
"width": 200,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": false
|
||||
},
|
||||
{
|
||||
"id": "platform2",
|
||||
"x": 700,
|
||||
"y": 500,
|
||||
"width": 150,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "linear",
|
||||
"points": [
|
||||
{"x": 700, "y": 300},
|
||||
{"x": 700, "y": 500}
|
||||
],
|
||||
"speed": 2.0,
|
||||
"wait_time": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform21",
|
||||
"x": 900,
|
||||
"y": 500,
|
||||
"width": 150,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "linear",
|
||||
"points": [
|
||||
{"x": 700, "y": 500},
|
||||
{"x": 800, "y": 500}
|
||||
],
|
||||
"speed": 2.0,
|
||||
"wait_time": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform3",
|
||||
"x": 1200,
|
||||
"y": 400,
|
||||
"width": 100,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "circular",
|
||||
"center": {"x": 1200, "y": 400},
|
||||
"radius": 100,
|
||||
"speed": 0.02,
|
||||
"clockwise": true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"enemies": [
|
||||
{
|
||||
"id": "enemy1",
|
||||
"type": "walker",
|
||||
"x": 500,
|
||||
"y": 620,
|
||||
"health": 1,
|
||||
"damage": 1,
|
||||
"behavior": "patrol",
|
||||
"patrol_points": [
|
||||
{"x": 400, "y": 620},
|
||||
{"x": 600, "y": 620}
|
||||
],
|
||||
"speed": 1.5,
|
||||
"sprite_sheet": "assets/map/enemy/walker_enemy.png"
|
||||
},
|
||||
{
|
||||
"id": "enemy2",
|
||||
"type": "flyer",
|
||||
"x": 1000,
|
||||
"y": 400,
|
||||
"health": 1,
|
||||
"damage": 1,
|
||||
"behavior": "chase",
|
||||
"detection_radius": 200,
|
||||
"speed": 2.0,
|
||||
"sprite_sheet": "assets/map/enemy/flying_enemy.png"
|
||||
},
|
||||
{
|
||||
"id": "enemy3",
|
||||
"type": "turret",
|
||||
"x": 1500,
|
||||
"y": 700,
|
||||
"health": 1,
|
||||
"damage": 1,
|
||||
"behavior": "stationary",
|
||||
"attack_interval": 2.0,
|
||||
"attack_range": 300,
|
||||
"sprite_sheet": "assets/map/enemy/turret_enemy.png"
|
||||
}
|
||||
],
|
||||
|
||||
"collectibles": [
|
||||
{
|
||||
"id": "coin1",
|
||||
"type": "coin",
|
||||
"x": 350,
|
||||
"y": 550,
|
||||
"value": 10,
|
||||
"sprite": "assets/map/collectibles/coin.png"
|
||||
},
|
||||
{
|
||||
"id": "power_up1",
|
||||
"type": "speed_boost",
|
||||
"x": 900,
|
||||
"y": 450,
|
||||
"duration": 5.0,
|
||||
"sprite": "assets/map/collectibles/speed_boost.png"
|
||||
}
|
||||
],
|
||||
|
||||
"checkpoints": [
|
||||
{
|
||||
"id": "checkpoint1",
|
||||
"x": 1200,
|
||||
"y": 760,
|
||||
"width": 50,
|
||||
"height": 50,
|
||||
"sprite": "assets/map/checkpoints/checkpoint.png"
|
||||
}
|
||||
],
|
||||
|
||||
"spawn_point": {
|
||||
"x": 50,
|
||||
"y": 700
|
||||
},
|
||||
|
||||
"exits": [
|
||||
{
|
||||
"x": 2300,
|
||||
"y": 700,
|
||||
"width": 50,
|
||||
"height": 80,
|
||||
"next_level": "Level 2",
|
||||
"sprite": "assets/map/exit/door.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
144
map/levels/2.json
Normal file
144
map/levels/2.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"name": "Level 2",
|
||||
"width": 2400,
|
||||
"height": 800,
|
||||
"background": "assets/map/background/forest_bg.jpg",
|
||||
"gravity": 1.0,
|
||||
|
||||
"ground": [
|
||||
{
|
||||
"id": "main_ground",
|
||||
"x": -1000,
|
||||
"y": 780,
|
||||
"width": 1800,
|
||||
"height": 200,
|
||||
"texture": "assets/map/platform/grass_texture.jpg"
|
||||
},
|
||||
{
|
||||
"id": "pit",
|
||||
"x": 800,
|
||||
"y": 780,
|
||||
"width": 200,
|
||||
"height": 20,
|
||||
"is_hole": true
|
||||
},
|
||||
{
|
||||
"id": "main_ground_2",
|
||||
"x": 1000,
|
||||
"y": 900,
|
||||
"width": 1800,
|
||||
"height": 200,
|
||||
"texture": "assets/map/platform/grass_texture.jpg"
|
||||
}
|
||||
],
|
||||
|
||||
"platforms": [
|
||||
{
|
||||
"id": "platform1",
|
||||
"x": 300,
|
||||
"y": 600,
|
||||
"width": 200,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": false
|
||||
},
|
||||
{
|
||||
"id": "platform2",
|
||||
"x": 700,
|
||||
"y": 500,
|
||||
"width": 150,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "linear",
|
||||
"points": [
|
||||
{"x": 700, "y": 300},
|
||||
{"x": 700, "y": 500}
|
||||
],
|
||||
"speed": 2.0,
|
||||
"wait_time": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform21",
|
||||
"x": 900,
|
||||
"y": 500,
|
||||
"width": 150,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "linear",
|
||||
"points": [
|
||||
{"x": 700, "y": 500},
|
||||
{"x": 800, "y": 500}
|
||||
],
|
||||
"speed": 2.0,
|
||||
"wait_time": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "platform3",
|
||||
"x": 1200,
|
||||
"y": 400,
|
||||
"width": 100,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "circular",
|
||||
"center": {"x": 1200, "y": 400},
|
||||
"radius": 100,
|
||||
"speed": 0.02,
|
||||
"clockwise": true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"collectibles": [
|
||||
{
|
||||
"id": "coin1",
|
||||
"type": "coin",
|
||||
"x": 350,
|
||||
"y": 550,
|
||||
"value": 10,
|
||||
"sprite": "assets/map/collectibles/coin.png"
|
||||
},
|
||||
{
|
||||
"id": "power_up1",
|
||||
"type": "speed_boost",
|
||||
"x": 900,
|
||||
"y": 450,
|
||||
"duration": 5.0,
|
||||
"sprite": "assets/map/collectibles/speed_boost.png"
|
||||
}
|
||||
],
|
||||
|
||||
"checkpoints": [
|
||||
{
|
||||
"id": "checkpoint1",
|
||||
"x": 1200,
|
||||
"y": 760,
|
||||
"width": 50,
|
||||
"height": 50,
|
||||
"sprite": "assets/map/checkpoints/checkpoint.png"
|
||||
}
|
||||
],
|
||||
|
||||
"spawn_point": {
|
||||
"x": 50,
|
||||
"y": 700
|
||||
},
|
||||
|
||||
"exits": [
|
||||
{
|
||||
"x": 2300,
|
||||
"y": 700,
|
||||
"width": 50,
|
||||
"height": 80,
|
||||
"next_level": "Level 3",
|
||||
"sprite": "assets/map/exit/door.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
|
||||
|
||||
class CheckpointDB:
|
||||
def __init__(self, db_file="checkpoint.db"):
|
||||
def __init__(self, db_file="game.db"):
|
||||
"""
|
||||
Initialize database connection for checkpoint management
|
||||
|
||||
@@ -42,11 +42,15 @@ class CheckpointDB:
|
||||
pos_x: X coordinate
|
||||
pos_y: Y coordinate
|
||||
"""
|
||||
self.cursor.execute(
|
||||
"INSERT OR REPLACE INTO checkpoints (map_name, pos_x, pos_y, timestamp) VALUES (?, ?, ?, strftime('%s'))",
|
||||
(map_name, pos_x, pos_y),
|
||||
)
|
||||
self.conn.commit()
|
||||
try:
|
||||
self.cursor.execute(
|
||||
"INSERT OR REPLACE INTO checkpoints (map_name, pos_x, pos_y, timestamp) VALUES (?, ?, ?, strftime('%s'))",
|
||||
(map_name, pos_x, pos_y),
|
||||
)
|
||||
self.conn.commit()
|
||||
print("Checkpoint saved")
|
||||
except Exception as e:
|
||||
print(f"Error saving checkpoint: {e}")
|
||||
|
||||
def get_checkpoint(self, map_name):
|
||||
"""
|
||||
@@ -58,6 +62,7 @@ class CheckpointDB:
|
||||
Returns:
|
||||
Tuple (x, y) if checkpoint exists, None otherwise
|
||||
"""
|
||||
print(map_name)
|
||||
self.cursor.execute(
|
||||
"SELECT pos_x, pos_y FROM checkpoints WHERE map_name = ?", (map_name,)
|
||||
)
|
||||
|
||||
99
src/Database/LevelDB.py
Normal file
99
src/Database/LevelDB.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
|
||||
class LevelDB:
|
||||
def __init__(self, db_file="game.db"):
|
||||
"""
|
||||
Initialize database connection for level progression management
|
||||
|
||||
Args:
|
||||
db_file: SQLite database file path
|
||||
"""
|
||||
# Create database directory if it doesn't exist
|
||||
os.makedirs(
|
||||
os.path.dirname(db_file) if os.path.dirname(db_file) else ".", exist_ok=True
|
||||
)
|
||||
|
||||
self.connection = sqlite3.connect(db_file)
|
||||
self.cursor = self.connection.cursor()
|
||||
self.create_unlocked_levels_table()
|
||||
|
||||
def create_unlocked_levels_table(self):
|
||||
"""Create the table to store unlocked levels if it doesn't exist"""
|
||||
self.cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS unlocked_levels (
|
||||
level_number INTEGER PRIMARY KEY,
|
||||
unlocked INTEGER DEFAULT 0,
|
||||
timestamp TEXT DEFAULT (strftime('%s'))
|
||||
)
|
||||
"""
|
||||
)
|
||||
self.connection.commit()
|
||||
|
||||
def is_level_unlocked(self, level_number):
|
||||
"""
|
||||
Check if a specific level is unlocked
|
||||
|
||||
Args:
|
||||
level_number: Level number to check
|
||||
|
||||
Returns:
|
||||
bool: True if level is unlocked, False otherwise
|
||||
"""
|
||||
# Level 1 is always unlocked
|
||||
if level_number == 1:
|
||||
return True
|
||||
|
||||
self.cursor.execute(
|
||||
"SELECT unlocked FROM unlocked_levels WHERE level_number = ?",
|
||||
(level_number,),
|
||||
)
|
||||
result = self.cursor.fetchone()
|
||||
return bool(result and result[0])
|
||||
|
||||
def unlock_level(self, level_number):
|
||||
"""
|
||||
Unlock a specific level
|
||||
|
||||
Args:
|
||||
level_number: Level number to unlock
|
||||
"""
|
||||
self.cursor.execute(
|
||||
"INSERT OR REPLACE INTO unlocked_levels (level_number, unlocked) VALUES (?, 1)",
|
||||
(level_number,),
|
||||
)
|
||||
self.connection.commit()
|
||||
print(f"Level {level_number} unlocked")
|
||||
|
||||
def get_all_unlocked_levels(self):
|
||||
"""
|
||||
Get a list of all unlocked level numbers
|
||||
|
||||
Returns:
|
||||
list: List of unlocked level numbers
|
||||
"""
|
||||
self.cursor.execute(
|
||||
"SELECT level_number FROM unlocked_levels WHERE unlocked = 1"
|
||||
)
|
||||
return [1] + [
|
||||
row[0] for row in self.cursor.fetchall()
|
||||
] # Level 1 + all unlocked levels
|
||||
|
||||
def reset_progress(self):
|
||||
"""
|
||||
Reset all progress, keeping only level 1 unlocked
|
||||
"""
|
||||
try:
|
||||
self.cursor.execute("DELETE FROM unlocked_levels")
|
||||
self.connection.commit()
|
||||
self.unlock_level(1) # Always unlock level 1
|
||||
print("Level progress reset successfully")
|
||||
except Exception as e:
|
||||
print(f"Error resetting level progress: {e}")
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
@@ -39,7 +39,6 @@ class Checkpoint(Entity):
|
||||
if not hasattr(self, "original_surf"):
|
||||
self.original_surf = self.surf.copy()
|
||||
self.surf.fill(self.activated_color)
|
||||
|
||||
# Save checkpoint to database
|
||||
self.db.save_checkpoint(self.map_name, self.pos.x, self.pos.y)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import pygame
|
||||
|
||||
|
||||
class Button:
|
||||
def __init__(self, text, x, y, width, height, action=None):
|
||||
def __init__(self, text, x, y, width, height, action=None, locked=False):
|
||||
self.text = text
|
||||
self.x = x
|
||||
self.y = y
|
||||
@@ -11,25 +11,38 @@ class Button:
|
||||
self.height = height
|
||||
self.action = action
|
||||
self.hover = False
|
||||
self.locked = locked
|
||||
self.rect = pygame.Rect(x, y, width, height)
|
||||
|
||||
def draw(self, surface, font):
|
||||
# Button colors
|
||||
color = (100, 149, 237) if self.hover else (65, 105, 225)
|
||||
border_color = (255, 255, 255)
|
||||
if self.locked:
|
||||
bg_color = (100, 100, 100)
|
||||
text_color = (200, 200, 200)
|
||||
elif self.hover:
|
||||
bg_color = (100, 100, 255)
|
||||
text_color = (255, 255, 255)
|
||||
else:
|
||||
bg_color = (50, 50, 200)
|
||||
text_color = (255, 255, 255)
|
||||
|
||||
# Draw button with border
|
||||
pygame.draw.rect(surface, color, (self.x, self.y, self.width, self.height))
|
||||
pygame.draw.rect(
|
||||
surface, border_color, (self.x, self.y, self.width, self.height), 2
|
||||
)
|
||||
pygame.draw.rect(surface, bg_color, self.rect, border_radius=10)
|
||||
pygame.draw.rect(surface, (0, 0, 0), self.rect, 2, border_radius=10)
|
||||
|
||||
# Draw text
|
||||
text_surf = font.render(self.text, True, (255, 255, 255))
|
||||
text_rect = text_surf.get_rect(
|
||||
center=(self.x + self.width / 2, self.y + self.height / 2)
|
||||
)
|
||||
text_surf = font.render(self.text, True, text_color)
|
||||
text_rect = text_surf.get_rect(center=self.rect.center)
|
||||
surface.blit(text_surf, text_rect)
|
||||
|
||||
# Add lock icon if button is locked
|
||||
if self.locked:
|
||||
lock_text = font.render("🔒", True, (255, 255, 255))
|
||||
lock_rect = lock_text.get_rect(
|
||||
center=(self.rect.right - 20, self.rect.y + 20)
|
||||
)
|
||||
surface.blit(lock_text, lock_rect)
|
||||
|
||||
def is_hover(self, pos):
|
||||
return (
|
||||
self.x <= pos[0] <= self.x + self.width
|
||||
|
||||
200
src/Menu/LevelSelectMenu.py
Normal file
200
src/Menu/LevelSelectMenu.py
Normal file
@@ -0,0 +1,200 @@
|
||||
import pygame
|
||||
import os
|
||||
import re
|
||||
|
||||
from src.Database.LevelDB import LevelDB
|
||||
from src.Menu.Button import Button
|
||||
from src.game import clear_checkpoint_database
|
||||
|
||||
|
||||
class LevelSelectMenu:
|
||||
"""
|
||||
A menu for selecting game levels loaded from JSON files.
|
||||
Presents all available levels from the map/levels/ directory as buttons.
|
||||
"""
|
||||
|
||||
def __init__(self, game_resources):
|
||||
"""
|
||||
Initialize the level selection menu.
|
||||
|
||||
Args:
|
||||
game_resources: GameResources object containing game settings and resources
|
||||
"""
|
||||
self.game_resources = game_resources
|
||||
self.buttons = []
|
||||
self.levels = []
|
||||
|
||||
# Button dimensions
|
||||
self.button_width = 250
|
||||
self.button_height = 60
|
||||
self.button_spacing = 20
|
||||
|
||||
# Initialize database and get unlocked levels
|
||||
self.db = LevelDB()
|
||||
self.db.create_unlocked_levels_table()
|
||||
self.unlocked_levels = self.db.get_all_unlocked_levels()
|
||||
|
||||
# Scan for level files
|
||||
self._scan_levels()
|
||||
|
||||
# Generate level buttons
|
||||
self._create_buttons()
|
||||
|
||||
# Add back button and reset progress button
|
||||
self._add_navigation_buttons()
|
||||
|
||||
def _scan_levels(self):
|
||||
"""
|
||||
Scan the levels directory for JSON level files and sort them numerically.
|
||||
"""
|
||||
try:
|
||||
# Get all JSON files in the levels directory
|
||||
level_dir = "map/levels/"
|
||||
if not os.path.exists(level_dir):
|
||||
os.makedirs(level_dir) # Create directory if it doesn't exist
|
||||
|
||||
files = [f for f in os.listdir(level_dir) if f.endswith(".json")]
|
||||
|
||||
# Extract level numbers using regex and sort numerically
|
||||
level_pattern = re.compile(r"(\d+)\.json$")
|
||||
self.levels = []
|
||||
|
||||
for file in files:
|
||||
match = level_pattern.search(file)
|
||||
if match:
|
||||
level_number = int(match.group(1))
|
||||
self.levels.append((level_number, f"{level_dir}{file}"))
|
||||
|
||||
# Sort levels by number
|
||||
self.levels.sort(key=lambda x: x[0])
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error scanning levels: {e}")
|
||||
self.levels = []
|
||||
|
||||
def _create_buttons(self):
|
||||
"""
|
||||
Create buttons for each available level.
|
||||
"""
|
||||
# Calculate how many buttons can fit per row
|
||||
buttons_per_row = 3
|
||||
button_width_with_spacing = self.button_width + self.button_spacing
|
||||
|
||||
# Start position for the grid of buttons
|
||||
start_x = (
|
||||
self.game_resources.WIDTH
|
||||
- (button_width_with_spacing * min(buttons_per_row, len(self.levels) or 1))
|
||||
) // 2
|
||||
start_y = self.game_resources.HEIGHT // 3
|
||||
|
||||
# Create buttons for each level
|
||||
for i, (level_num, level_file) in enumerate(self.levels):
|
||||
# Calculate position in grid
|
||||
row = i // buttons_per_row
|
||||
col = i % buttons_per_row
|
||||
|
||||
x = start_x + (col * button_width_with_spacing)
|
||||
y = start_y + (row * (self.button_height + self.button_spacing))
|
||||
|
||||
# Check if level is unlocked
|
||||
is_unlocked = self.db.is_level_unlocked(level_num)
|
||||
button_text = f"Level {level_num}"
|
||||
button_color = None
|
||||
|
||||
# If locked, disable button functionality
|
||||
action = (
|
||||
{"action": "select_level", "level_file": level_file}
|
||||
if is_unlocked
|
||||
else None
|
||||
)
|
||||
|
||||
# Create button
|
||||
self.buttons.append(
|
||||
Button(
|
||||
button_text,
|
||||
x,
|
||||
y,
|
||||
self.button_width,
|
||||
self.button_height,
|
||||
action,
|
||||
locked=not is_unlocked,
|
||||
)
|
||||
)
|
||||
|
||||
def _add_navigation_buttons(self):
|
||||
"""
|
||||
Add navigation buttons (back and reset progress).
|
||||
"""
|
||||
# Back button
|
||||
self.buttons.append(
|
||||
Button(
|
||||
"Back",
|
||||
self.game_resources.WIDTH // 4 - self.button_width // 2,
|
||||
self.game_resources.HEIGHT - 100,
|
||||
self.button_width,
|
||||
self.button_height,
|
||||
"back_to_main",
|
||||
)
|
||||
)
|
||||
|
||||
# Reset progress button
|
||||
self.buttons.append(
|
||||
Button(
|
||||
"Reset Progress",
|
||||
3 * self.game_resources.WIDTH // 4 - self.button_width // 2,
|
||||
self.game_resources.HEIGHT - 100,
|
||||
self.button_width,
|
||||
self.button_height,
|
||||
"reset_progress",
|
||||
)
|
||||
)
|
||||
|
||||
def draw(self, surface):
|
||||
"""
|
||||
Draw the level selection menu.
|
||||
|
||||
Args:
|
||||
surface: Pygame surface to draw on
|
||||
"""
|
||||
# Draw title
|
||||
title = pygame.font.SysFont("Arial", 48).render(
|
||||
"Select Level", True, (0, 191, 255)
|
||||
)
|
||||
title_rect = title.get_rect(
|
||||
center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT // 6)
|
||||
)
|
||||
surface.blit(title, title_rect)
|
||||
|
||||
# Draw buttons
|
||||
for button in self.buttons:
|
||||
button.draw(surface, self.game_resources.font)
|
||||
|
||||
# Display message if no levels found
|
||||
if not self.levels:
|
||||
no_levels = pygame.font.SysFont("Arial", 32).render(
|
||||
"No levels found", True, (255, 0, 0)
|
||||
)
|
||||
no_levels_rect = no_levels.get_rect(
|
||||
center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT // 2)
|
||||
)
|
||||
surface.blit(no_levels, no_levels_rect)
|
||||
|
||||
def handle_event(self, event):
|
||||
"""
|
||||
Handle user input events.
|
||||
|
||||
Args:
|
||||
event: Pygame event to process
|
||||
|
||||
Returns:
|
||||
dict/str/None: Action to perform based on button clicked, or None
|
||||
"""
|
||||
for button in self.buttons:
|
||||
action = button.handle_event(event)
|
||||
if action:
|
||||
if action == "reset_progress":
|
||||
# Clear checkpoint database
|
||||
clear_checkpoint_database()
|
||||
return None # Stay in the level select menu
|
||||
return action
|
||||
return None
|
||||
@@ -19,7 +19,7 @@ class Menu:
|
||||
start_y,
|
||||
button_width,
|
||||
button_height,
|
||||
"play",
|
||||
"level_select",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
13
src/game.py
13
src/game.py
@@ -7,7 +7,7 @@ from src.Map.parser import MapParser
|
||||
from src.Database.CheckpointDB import CheckpointDB
|
||||
|
||||
|
||||
def initialize_game(game_resources, map_file="map_test.json"):
|
||||
def initialize_game(game_resources, map_file="map/levels/1.json"):
|
||||
"""
|
||||
Initialize game with map from JSON file
|
||||
|
||||
@@ -60,16 +60,6 @@ def initialize_game(game_resources, map_file="map_test.json"):
|
||||
)
|
||||
|
||||
|
||||
def reset_game(game_resources):
|
||||
"""Reset the game to initial state"""
|
||||
# Reload game objects
|
||||
player, _, platforms, all_sprites, background, checkpoints, exits = initialize_game(
|
||||
game_resources, "map_test.json"
|
||||
)
|
||||
|
||||
return player, platforms, all_sprites, background, checkpoints
|
||||
|
||||
|
||||
def reset_game_with_checkpoint(map_name, game_resources):
|
||||
"""
|
||||
Reset the game and respawn player at checkpoint if available
|
||||
@@ -103,6 +93,7 @@ def clear_checkpoint_database():
|
||||
try:
|
||||
db = CheckpointDB()
|
||||
db.clear_all()
|
||||
db.close()
|
||||
print("Checkpoint database cleared successfully")
|
||||
except Exception as e:
|
||||
print(f"Error clearing checkpoint database: {e}")
|
||||
|
||||
Reference in New Issue
Block a user