commit c2f76d4222d54cf9c3b3dc26f4e6909cf4216cad Author: Carlo Ghisolfi Date: Sun Oct 4 19:17:16 2026 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29b232c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*pycache* diff --git a/assets/ARCO.ttf b/assets/ARCO.ttf new file mode 100644 index 0000000..a1abb0e Binary files /dev/null and b/assets/ARCO.ttf differ diff --git a/assets/background.png b/assets/background.png new file mode 100644 index 0000000..d1b15b1 Binary files /dev/null and b/assets/background.png differ diff --git a/assets/blue_arrive.svg b/assets/blue_arrive.svg new file mode 100644 index 0000000..da64686 --- /dev/null +++ b/assets/blue_arrive.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/assets/blue_pane.png b/assets/blue_pane.png new file mode 100644 index 0000000..eeebbfe Binary files /dev/null and b/assets/blue_pane.png differ diff --git a/assets/blue_robot.svg b/assets/blue_robot.svg new file mode 100644 index 0000000..aa0904f --- /dev/null +++ b/assets/blue_robot.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/blue_robot_inactive.svg b/assets/blue_robot_inactive.svg new file mode 100644 index 0000000..90d7732 --- /dev/null +++ b/assets/blue_robot_inactive.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/blue_text.png b/assets/blue_text.png new file mode 100644 index 0000000..cae32a5 Binary files /dev/null and b/assets/blue_text.png differ diff --git a/assets/bw_btn.png b/assets/bw_btn.png new file mode 100644 index 0000000..b74b6eb Binary files /dev/null and b/assets/bw_btn.png differ diff --git a/assets/bw_dark_btn.png b/assets/bw_dark_btn.png new file mode 100644 index 0000000..32420d2 Binary files /dev/null and b/assets/bw_dark_btn.png differ diff --git a/assets/completed_card.png b/assets/completed_card.png new file mode 100644 index 0000000..4b0d711 Binary files /dev/null and b/assets/completed_card.png differ diff --git a/assets/delete_btn.png b/assets/delete_btn.png new file mode 100644 index 0000000..49da3e9 Binary files /dev/null and b/assets/delete_btn.png differ diff --git a/assets/delete_dark_btn.png b/assets/delete_dark_btn.png new file mode 100644 index 0000000..29a23c4 Binary files /dev/null and b/assets/delete_dark_btn.png differ diff --git a/assets/floor.svg b/assets/floor.svg new file mode 100644 index 0000000..ec38c35 --- /dev/null +++ b/assets/floor.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + diff --git a/assets/fw_btn.png b/assets/fw_btn.png new file mode 100644 index 0000000..1383ced Binary files /dev/null and b/assets/fw_btn.png differ diff --git a/assets/fw_dark_btn.png b/assets/fw_dark_btn.png new file mode 100644 index 0000000..562900a Binary files /dev/null and b/assets/fw_dark_btn.png differ diff --git a/assets/go_btn.png b/assets/go_btn.png new file mode 100644 index 0000000..c4ff959 Binary files /dev/null and b/assets/go_btn.png differ diff --git a/assets/go_dark_btn.png b/assets/go_dark_btn.png new file mode 100644 index 0000000..f187a92 Binary files /dev/null and b/assets/go_dark_btn.png differ diff --git a/assets/green_arrive.svg b/assets/green_arrive.svg new file mode 100644 index 0000000..23b397e --- /dev/null +++ b/assets/green_arrive.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/assets/green_pane.png b/assets/green_pane.png new file mode 100644 index 0000000..40052c6 Binary files /dev/null and b/assets/green_pane.png differ diff --git a/assets/green_robot.svg b/assets/green_robot.svg new file mode 100644 index 0000000..4f17a3c --- /dev/null +++ b/assets/green_robot.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/green_robot_inactive.svg b/assets/green_robot_inactive.svg new file mode 100644 index 0000000..7f1878c --- /dev/null +++ b/assets/green_robot_inactive.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/green_text.png b/assets/green_text.png new file mode 100644 index 0000000..def810a Binary files /dev/null and b/assets/green_text.png differ diff --git a/assets/gui.svg b/assets/gui.svg new file mode 100644 index 0000000..2ea339d --- /dev/null +++ b/assets/gui.svg @@ -0,0 +1,4536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + completed! + + completed! + + + + + + + + + + retry + retry + retry + + + + + + + + + + + + menu + menu + + + + + + + + + + + + + + + + + + + + + + + + + + menu + menu + + + + + + + + + play + play + + + + + + + + + + + + + quit + quit + + + + + + + + + + + + + + + + + + delete + delete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + paused + paused + + + + + + + + + + + resume + Resume + + + + + + + + + + + + + + menu + menu + + + + + + + + + + + + + + + + + + + + + + + + + + level select + level select + + + + + + + + + menu + menu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + play + play + + + + + + + + + + + + + + delete + delete + + + + + + + + + + + + + + + + + + + + + + + quit + quit + + + + + + + + + + + + + + + + + + + resume + Resume + + + + + + + + + + + + + menu + menu + + + + + + + + + + + + + + + + + + + retry + retry + retry + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00:00:00 + 00:00:00 + 00:00:00 + + + + + + + + + + + + + + diff --git a/assets/gui.svg.2026_10_02_19_17_27.0.svg b/assets/gui.svg.2026_10_02_19_17_27.0.svg new file mode 100644 index 0000000..1016b78 --- /dev/null +++ b/assets/gui.svg.2026_10_02_19_17_27.0.svg @@ -0,0 +1,4087 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + paused + paused + + + + + + completed! + + completed! + + completed! + + + + level select + level select + + + play + play + + + + + + retry + retry + retry + + + + + retry + retry + retry + + + + + resume + Resume + + + + + resume + Resume + + + + + quit + quit + + + + + + + + + + + menu + menu + + + + + + + + + + + menu + menu + + + + + + + + + + + menu + menu + + + + + + + + + + + menu + menu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + menu + menu + + + play + play + + + + + + quit + quit + + + + + + + + + + + delete + delete + + + + + + + + + + + + + + + + delete + delete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/locked.png b/assets/locked.png new file mode 100644 index 0000000..d015fa4 Binary files /dev/null and b/assets/locked.png differ diff --git a/assets/menu_btn.png b/assets/menu_btn.png new file mode 100644 index 0000000..f373574 Binary files /dev/null and b/assets/menu_btn.png differ diff --git a/assets/menu_card.png b/assets/menu_card.png new file mode 100644 index 0000000..f3dee04 Binary files /dev/null and b/assets/menu_card.png differ diff --git a/assets/menu_dark_btn.png b/assets/menu_dark_btn.png new file mode 100644 index 0000000..535cd36 Binary files /dev/null and b/assets/menu_dark_btn.png differ diff --git a/assets/orange_door.svg b/assets/orange_door.svg new file mode 100644 index 0000000..00ed029 --- /dev/null +++ b/assets/orange_door.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/orange_press.svg b/assets/orange_press.svg new file mode 100644 index 0000000..66ab1bf --- /dev/null +++ b/assets/orange_press.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/assets/panel.png b/assets/panel.png new file mode 100644 index 0000000..7bda92b Binary files /dev/null and b/assets/panel.png differ diff --git a/assets/paused_card.png b/assets/paused_card.png new file mode 100644 index 0000000..f472b43 Binary files /dev/null and b/assets/paused_card.png differ diff --git a/assets/play_btn.png b/assets/play_btn.png new file mode 100644 index 0000000..ba9188e Binary files /dev/null and b/assets/play_btn.png differ diff --git a/assets/play_dark_btn.png b/assets/play_dark_btn.png new file mode 100644 index 0000000..e0d8833 Binary files /dev/null and b/assets/play_dark_btn.png differ diff --git a/assets/quit_btn.png b/assets/quit_btn.png new file mode 100644 index 0000000..3f415bc Binary files /dev/null and b/assets/quit_btn.png differ diff --git a/assets/quit_dark_btn.png b/assets/quit_dark_btn.png new file mode 100644 index 0000000..aa50ecc Binary files /dev/null and b/assets/quit_dark_btn.png differ diff --git a/assets/red_door.svg b/assets/red_door.svg new file mode 100644 index 0000000..41dbf2c --- /dev/null +++ b/assets/red_door.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/red_press.svg b/assets/red_press.svg new file mode 100644 index 0000000..6766b78 --- /dev/null +++ b/assets/red_press.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/assets/resume_btn.png b/assets/resume_btn.png new file mode 100644 index 0000000..8f08ccb Binary files /dev/null and b/assets/resume_btn.png differ diff --git a/assets/resume_dark_btn.png b/assets/resume_dark_btn.png new file mode 100644 index 0000000..254dd7f Binary files /dev/null and b/assets/resume_dark_btn.png differ diff --git a/assets/retry_btn.png b/assets/retry_btn.png new file mode 100644 index 0000000..d33715f Binary files /dev/null and b/assets/retry_btn.png differ diff --git a/assets/retry_dark_btn.png b/assets/retry_dark_btn.png new file mode 100644 index 0000000..a8560d4 Binary files /dev/null and b/assets/retry_dark_btn.png differ diff --git a/assets/select_card.png b/assets/select_card.png new file mode 100644 index 0000000..3374621 Binary files /dev/null and b/assets/select_card.png differ diff --git a/assets/teal_door.svg b/assets/teal_door.svg new file mode 100644 index 0000000..2351ea6 --- /dev/null +++ b/assets/teal_door.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/teal_press.svg b/assets/teal_press.svg new file mode 100644 index 0000000..426ce0f --- /dev/null +++ b/assets/teal_press.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/assets/tiles_and_sprites.svg b/assets/tiles_and_sprites.svg new file mode 100644 index 0000000..ed811b4 --- /dev/null +++ b/assets/tiles_and_sprites.svg @@ -0,0 +1,759 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/timer.png b/assets/timer.png new file mode 100644 index 0000000..448980b Binary files /dev/null and b/assets/timer.png differ diff --git a/assets/unlocked.png b/assets/unlocked.png new file mode 100644 index 0000000..a13902d Binary files /dev/null and b/assets/unlocked.png differ diff --git a/assets/wall.svg b/assets/wall.svg new file mode 100644 index 0000000..07a5e55 --- /dev/null +++ b/assets/wall.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..da623cc --- /dev/null +++ b/constants.py @@ -0,0 +1,43 @@ +# CONSTANTS +FPS = 60 +SCALE_FACTOR = 1 + +# Tiles +TILE_SIZE = 32 + +# SCREEN AND PANEL +WIDTH = 1280 +HEIGHT = 720 +GAME_PANEL_W = 960 +GAME_PANEL_H = 640 +X_OFFSET = WIDTH/2 - GAME_PANEL_W / 2 +Y_OFFSET = HEIGHT/2 - GAME_PANEL_H / 2 + +PANEL_WIDTH = 120 +PANEL_HEIGHT = 120 +PANEL_T_HEIGHT = 50 +PANEL_T_WIDTH = 140 + + +# WALL_COLOR = (50, 50, 50) +# FLOOR_COLOR = (200, 200, 200) +# RED_DOOR = (200, 0, 0) +# RED_PLATE = (100, 0, 0) +# BLUE_ARRIVE = (0, 0, 100) +# GREEN_ARRIVE = (0, 100, 0) +# BG_COLOR = (255, 0, 255) + +# BUTTONS +MAX_BTN = 3 +BTN_WIDTH = 232 +BTN_HEIGHT = 82 + +# COLORS +BLUE = (43, 69, 104) +GREEN= (43, 104, 43) +RED = (93, 38, 38) + +LIGHT = (100, 100, 100) +DARK = (64, 56, 43) +DARK_TEXT = (62,51,34) +WHITE = (255, 255, 255) diff --git a/db_utils.py b/db_utils.py new file mode 100644 index 0000000..05d61c0 --- /dev/null +++ b/db_utils.py @@ -0,0 +1,112 @@ +import sqlite3 +from pathlib import * +import json + +def init_db(): + conn = sqlite3.connect("progress.db") + cursor = conn.cursor() + + for path in Path("./levels").glob("*.json"): + with open(path, "r") as file: + level = json.load(file) + + level_id = level["id"] + + # Initialize progress on levels not already in the table + cursor.execute( + """ + INSERT OR IGNORE INTO player_progress + (id, unlocked, completed, timer) + VALUES (?, 0, 0, 0) + """, + (level_id,) + ) + # For end + + # Set the unlocked property for the first level + cursor.execute( + """ + UPDATE player_progress + SET unlocked = TRUE + WHERE id = ? + """, + ("001",) + ) + + conn.commit() + conn.close() + +def get_levels(): + levels = sorted(Path("./levels").glob("*.json")) + return levels + +def reset_db(): + conn = sqlite3.connect("progress.db") + cursor = conn.cursor() + + cursor.execute( + """ + DELETE FROM player_progress + """ + ) + + conn.commit() + conn.close() + +def get_progress(): + conn = sqlite3.connect("progress.db") + cursor = conn.cursor() + + # Set the completed property fand timer + cursor.execute( + """ + SELECT unlocked, timer FROM player_progress + ORDER BY id ASC + """, + ) + result = cursor.fetchall() + + conn.close() + + return result + + +def save_progress(level_state): + conn = sqlite3.connect("progress.db") + cursor = conn.cursor() + + level_id = level_state.level.id + + # Set the completed property fand timer + cursor.execute( + """ + UPDATE player_progress + SET completed = TRUE, timer = ? + WHERE id = ? + """, + (level_state.final_time, level_id,) + ) + + unlock_level_id = None + next_level_number = level_state.level.level_number + 1 + + for path in Path("./levels").glob("*.json"): + if int(path.name.split("_")[0]) == next_level_number: + with open(path, "r") as file: + level = json.load(file) + unlock_level_id = level["id"] + break + + if unlock_level_id is not None: + # Set the unlocked property for the next level + cursor.execute( + """ + UPDATE player_progress + SET unlocked = TRUE + WHERE id = ? + """, + (unlock_level_id,) + ) + + conn.commit() + conn.close() \ No newline at end of file diff --git a/gamestate.py b/gamestate.py new file mode 100644 index 0000000..1022919 --- /dev/null +++ b/gamestate.py @@ -0,0 +1,100 @@ +# libraries +from datetime import timedelta + +# project modules +import db_utils as db +import db_utils +from new_gui import * + +# ----------------------------------------------------- +# Enum and Tables +# ----------------------------------------------------- +class State(Enum): + MENU = 0 + PLAY = 1 + PAUSE = 2 + SELECT = 3 + COMPLETE = 4 + +# ------------------------------------------------------ +# Class GameState +# - Represents the game state (playing, pause, menu...) +# - creates and keeps the different views +# ------------------------------------------------------ +class GameState: + def __init__(self): + self.running = True + self.state = State.MENU + self.level_state = None + + # Game time keeping + self.play_start = 0 + self.paused_time = 0 + self.pause_start = 0 + self.elapsed = 0 + + # Views + self.start_menu = self.create_start_menu() + self.pause_menu = self.create_pause_menu() + self.select_menu = self.create_select_menu() + # It will get created at the end of each level + self.completed_menu = None + + def update(self, ticks): + # update elapsed playing time + if self.state == State.PLAY: + self.elapsed = ticks - self.play_start - self.paused_time + + # If the current level has been completed. This block of code is executed ONCE with the last check of the if + if self.level_state != None and self.level_state.completed == True and self.state != State.COMPLETE: + # save total time inside the level state + self.level_state.final_time = self.elapsed + self.state = State.COMPLETE + self.completed_menu = self.create_completed_menu() + + self.save_progress() + self.select_menu = self.create_select_menu() + + def save_progress(self): + # Save progress in db and unlock next level + db.save_progress(self.level_state) + + # Functions that create the views + def create_pause_menu(self): + menu = MenuPage(MenuType.PAUSE) + menu.add_top_text("Don't worry, the clock isn't ticking!") + menu.add_button("playing", ButtonTypes.RESUME) + menu.add_button("select_level", ButtonTypes.MENU) + return menu + + def create_start_menu(self): + menu = MenuPage(MenuType.START) + menu.add_button("select_level", ButtonTypes.PLAY) + menu.add_button("delete", ButtonTypes.DELETE) + menu.add_button("quit_game", ButtonTypes.QUIT) + return menu + + def create_completed_menu(self): + # Text with the time formatted + timer = timedelta(milliseconds=self.level_state.final_time) + text = f"Timer: {str(timer).split(".")[0]}" + + menu = MenuPage(MenuType.COMPLETED) + menu.add_top_text("Play the level again to beat your time!") + menu.add_bottom_text(text) + menu.add_button("retry", ButtonTypes.RETRY) + menu.add_button("select_level", ButtonTypes.MENU) + return menu + + def create_select_menu(self): + level_paths = db_utils.get_levels() + progress = db_utils.get_progress() + menu = Menu(MenuType.SELECT) + # Adding buttons based on level files + for info in zip(level_paths, progress): + # print(f"Path: {info[0]}, Completato:{info[1][0]}, Tempo:{info[1][1]}") + time_text = str(timedelta(milliseconds=info[1][1])).split(".")[0] + text = f"{info[0].name.split('.')[0].split('_')[1]} - {time_text}" + menu.add_entry(text, (info[0],info[1][0]), EntryState.UNLOCKED if int(info[1][0]) == 1 else EntryState.LOCKED) + return menu + diff --git a/levels/00_tutorial.json b/levels/00_tutorial.json new file mode 100644 index 0000000..32a4b58 --- /dev/null +++ b/levels/00_tutorial.json @@ -0,0 +1,17 @@ +{ + "id": "001", + "map": [ + ["#","#","#","#","#","#","#","#","#","#"], + ["#","G",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".","#",".",".",".","#"], + ["#",".","b",".",".","R",".",".",".","#"], + ["#",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".","#",".","g",".","#"], + ["#",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".","#",".",".",".","#"], + ["#","r",".",".",".","#",".",".","B","#"], + ["#","#","#","#","#","#","#","#","#","#"] + ] +} \ No newline at end of file diff --git a/levels/01_livello1.json b/levels/01_livello1.json new file mode 100644 index 0000000..7277a0e --- /dev/null +++ b/levels/01_livello1.json @@ -0,0 +1,25 @@ +{ + "id": "002", + "map":[ + ["#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#"], + ["#","G",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".","b",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","R",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".","g",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".",".","#"], + ["#","r",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".",".","#",".",".","B","#"], + ["#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#"] + ] +} \ No newline at end of file diff --git a/levels/02_livello2.json b/levels/02_livello2.json new file mode 100644 index 0000000..020ef93 --- /dev/null +++ b/levels/02_livello2.json @@ -0,0 +1,4 @@ +{ + "id": "003", + "map": [[]] +} \ No newline at end of file diff --git a/levels/03_livello3.json b/levels/03_livello3.json new file mode 100644 index 0000000..658dfa9 --- /dev/null +++ b/levels/03_livello3.json @@ -0,0 +1,4 @@ +{ + "id": "004", + "map": [[]] +} \ No newline at end of file diff --git a/levels/04_livello4.json b/levels/04_livello4.json new file mode 100644 index 0000000..9460830 --- /dev/null +++ b/levels/04_livello4.json @@ -0,0 +1,4 @@ +{ + "id": "005", + "map": [[]] +} \ No newline at end of file diff --git a/levelstate.py b/levelstate.py new file mode 100644 index 0000000..8e4a581 --- /dev/null +++ b/levelstate.py @@ -0,0 +1,365 @@ +# libraries +import pygame +import numpy as np +import json + +# project modules +from constants import * +from player import * +from new_gui import * + +# ------------------------------------------------------------------------------- +# Enum and Tables +# ------------------------------------------------------------------------------- +class TileTypes(Enum): + FLOOR = 0 + WALL = 1 + RED_PLATE = 2 + RED_DOOR = 3 + ORANGE_PLATE = 4 + ORANGE_DOOR = 5 + TEAL_PLATE = 6 + TEAL_DOOR = 7 + BLUE_ARRIVE = 8 + GREEN_ARRIVE = 9 + +TILE_FILES = { + TileTypes.FLOOR: "assets/floor.svg", + TileTypes.WALL : "assets/wall.svg", + TileTypes.RED_PLATE : "assets/red_press.svg", + TileTypes.RED_DOOR : "assets/red_door.svg", + TileTypes.ORANGE_PLATE : "assets/orange_press.svg", + TileTypes.ORANGE_DOOR : "assets/orange_door.svg", + TileTypes.TEAL_PLATE : "assets/teal_press.svg", + TileTypes.TEAL_DOOR : "assets/teal_door.svg", + TileTypes.BLUE_ARRIVE : "assets/blue_arrive.svg", + TileTypes.GREEN_ARRIVE : "assets/green_arrive.svg" +} + +class GamePanel(Enum): + BLUE_PANE = 0 + BLUE_TEXT = 1 + GREEN_TEXT = 2 + GREEN_PANE = 3 + TIMER = 4 + +PANEL_FILES = { + GamePanel.BLUE_PANE: "assets/blue_pane.png", + GamePanel.BLUE_TEXT: "assets/blue_text.png", + GamePanel.GREEN_PANE: "assets/green_pane.png", + GamePanel.GREEN_TEXT: "assets/green_text.png", + GamePanel.TIMER: "assets/timer.png" +} + +class Direction(Enum): + HORIZ = 0 + VERT = 1 + +# ------------------------------------------------------------------------------- +# Class Level +# - represents the static data of the current level being played +# ------------------------------------------------------------------------------- +class Level: + def __init__(self, path): + # Saving the order number of the level + self.level_number = int(path.name.split("_")[0]) + + # Level data file loading + with open(path, "r") as file: + level_data = json.load(file) + self.map = np.array(level_data["map"]) + self.id = level_data["id"] + + # LOADING TILE IMAGES + target_size = (TILE_SIZE, TILE_SIZE) + self.floor_tile = self.load_tile(TileTypes.FLOOR ,target_size) + self.wall_tile = self.load_tile(TileTypes.WALL ,target_size) + + self.red_press = self.load_tile(TileTypes.RED_PLATE ,target_size) + self.red_door = self.load_tile(TileTypes.RED_DOOR ,target_size) + self.orange_press = self.load_tile(TileTypes.ORANGE_PLATE ,target_size) + self.orange_door = self.load_tile(TileTypes.ORANGE_DOOR ,target_size) + self.teal_press = self.load_tile(TileTypes.TEAL_PLATE ,target_size) + self.teal_door = self.load_tile(TileTypes.TEAL_DOOR ,target_size) + + self.blue_arrive = self.load_tile(TileTypes.BLUE_ARRIVE ,target_size) + self.green_arrive = self.load_tile(TileTypes.GREEN_ARRIVE ,target_size) + + self.doors, self.plates = self.load_objs() + + # For now this method does this with the map, in the future they will be encoded differently in the json + # maybe already in arrays to make everything easier + def load_objs(self): + doors = {} + plates = {} + for (row, col), character in np.ndenumerate(self.map): + if character in ('R'): + # Todo: choose type based on character + doors[character] = Door(col, row, TileTypes.RED_DOOR) + if character.lower() in plates: + plates[character.lower()].set_door(doors[character]) + if character in ('r'): + # Todo: choose type based on character + plates[character] = PressurePlate(col, row, TileTypes.RED_PLATE) + if character.upper() in doors: + plates[character].set_door(doors[character.upper()]) + + + #for (row, col), character in np.ndenumerate(self.map): + # if character == 'r': + # plates.append(PressurePlate(col, row, TileTypes.RED_PLATE)) + + return doors, plates + + + def load_tile(self, type, target_size): + return pygame.transform.smoothscale(pygame.image.load(TILE_FILES.get(type, TILE_FILES[TileTypes.FLOOR])).convert_alpha(), target_size) + + def draw(self, screen, pressing): + for (row, col), character in np.ndenumerate(self.map): + + # Convert grid coordinates to pixel coordinates + x = col * TILE_SIZE + X_OFFSET + y = row * TILE_SIZE + Y_OFFSET + + # Create the rectangle boundary for the tile + rect = pygame.Rect(x, y, TILE_SIZE, TILE_SIZE) + + # Draw the appropriate tile color + if character == '#': + screen.blit(self.wall_tile, rect) + # elif character == 'R' and pressing != "red": + # screen.blit(self.red_door, rect) + # elif character == 'r': + # screen.blit(self.red_press, rect) + elif character == 'B': + screen.blit(self.blue_arrive, rect) + elif character == 'G': + screen.blit(self.green_arrive, rect) + else: + screen.blit(self.floor_tile, rect) + + for element in list(self.doors.values()) + list(self.plates.values()): + element.draw(screen) + +# ------------------------------------------------------------------------------- +# Class LevelState +# - represents the state of the current level being played +# - it manages all the gameplay logic and mechanics +# ------------------------------------------------------------------------------- +class LevelState: + def __init__(self, level): + self.level = level + + # Players setup + b_rows, b_cols = np.where(level.map == 'b') + g_rows, g_cols = np.where(level.map == 'g') + + blue_y, blue_x = b_rows[0], b_cols[0] + green_y, green_x = g_rows[0], g_cols[0] + self.players = [Player(blue_x, blue_y, True, PlayerTypes.BLUE), Player(green_x, green_y, False, PlayerTypes.GREEN)] + self.curr_player_i = 0 + self.player = self.players[self.curr_player_i] + + # Sprite group for ease + self.all_sprites = pygame.sprite.Group() + self.all_sprites.add(self.players) + + # state variables + self.pressing = "none" + self.blue_arrive = False + self.green_arrive = False + + # Completion and final time + self.completed = False + self.final_time = 0 + + # GUI ELEMENTS + self.blue_pane = self.load_gui(GamePanel.BLUE_PANE) + self.green_pane = self.load_gui(GamePanel.GREEN_PANE) + self.blue_text = self.load_gui(GamePanel.BLUE_TEXT) + self.green_text = self.load_gui(GamePanel.GREEN_TEXT) + self.timer = self.load_gui(GamePanel.TIMER) + + def load_gui(self, element): + return pygame.image.load(PANEL_FILES.get(element, None)).convert_alpha() + + def change_player(self): + self.player.active = False + self.curr_player_i = 1 - self.curr_player_i + self.player = self.players[self.curr_player_i] + self.player.active = True + + def try_move(self, dir, x, y, speed): + if dir == Direction.HORIZ: + next_x = x + speed + if (self.level.map[y, next_x] == '#' or + (self.level.map[y, next_x] == 'R' and self.pressing != "red") or + (next_x == self.players[1-self.curr_player_i].x and y == self.players[1-self.curr_player_i].y)): + return x + + return next_x + else: + next_y = y + speed + if (self.level.map[next_y, x] == '#' or + (self.level.map[next_y, x] == 'R' and self.pressing != "red") or + (next_y == self.players[1-self.curr_player_i].y and x == self.players[1-self.curr_player_i].x)): + return y + + return next_y + + def update(self): + # to facilitate usage + p1 = self.players[0] + p2 = self.players[1] + + # check for pressure on plates + # TODO: Add the other types of plates to press + if (self.level.map[p1.y, p1.x] == "r" and p1.powers[p1.current_power_i] == Powers.PRESS.value) or \ + (self.level.map[p2.y, p2.x] == "r" and p2.powers[p2.current_power_i] == Powers.PRESS.value): + self.pressing = "red" + else: + self.pressing = "none" + + for plate in self.level.plates.values(): + if((p1.rect.x, p1.rect.y) == (plate.x, plate.y) and + p1.powers[p1.current_power_i] == Powers.PRESS.value) or ((p2.rect.x, p2.rect.y) == (plate.x, plate.y) and + p2.powers[p2.current_power_i] == Powers.PRESS.value): + plate.set_pressed(True) + else: + plate.set_pressed(False) + + # check for arrival + self.blue_arrive = self.level.map[p1.y, p1.x] == "B" + self.green_arrive = self.level.map[p2.y, p2.x] == "G" + + # Check completed condition + if(self.blue_arrive and self.green_arrive): + self.completed = True + + def draw(self, screen): + self.level.draw(screen, self.pressing) + + def draw_gui(self, screen, time, font): + y = Y_OFFSET + + # Blue pane, then blue text box with the player's power + y = self._blit_panel(screen, self.blue_pane, y) + y = self._blit_panel(screen, self.blue_text, y, self.players[0].current_power, BLUE, font) + + # Green pane, then green text box with the player's power + y = self._blit_panel(screen, self.green_pane, y) + y = self._blit_panel(screen, self.green_text, y, self.players[1].current_power, GREEN, font) + + # Timer box with the elapsed seconds + self._blit_panel(screen, self.timer, y, str(time).split('.')[0], RED, font) + + def _blit_panel(self, screen, image, y, text=None, color=None, font=None): + x = (X_OFFSET - image.get_width()) / 2 + screen.blit(image, (x, y)) + + if text is not None: + label = TextArea(text, color) + label.place(x + image.get_width() / 2, y + image.get_height() / 2) + label.draw(screen, None, font, anchor="center") + + return y + image.get_height() + +# --------------------------------------------------------------------- +# Class MapElement +# --------------------------------------------------------------------- +# This is more of an abstract class that grants common method signature +# and behaviour. +# It inherits from Sprite so that I can add it to the group and +# it already has the draw method implemented +# --------------------------------------------------------------------- +class MapElement(pygame.sprite.Sprite): + def __init__(self, col, row, type): + super().__init__() + self.col = col + self.row = row + self.x = col * TILE_SIZE + X_OFFSET + self.y = row * TILE_SIZE + Y_OFFSET + self.image = self.load_image(type) + self.rect = self.image.get_rect() + self.rect.topleft = (self.x, self.y) + + def load_image(self, type): + return pygame.transform.smoothscale(pygame.image.load(TILE_FILES.get(type, TILE_FILES[TileTypes.FLOOR])).convert_alpha(), (TILE_SIZE, TILE_SIZE)) + + def draw(self, screen): + screen.blit(self.image, self.rect) +# ---------------------------------------------------------------------- +# Class PressurePlate +# ---------------------------------------------------------------------- +class PressurePlate(MapElement): + def __init__(self, col, row, type): + super().__init__(col, row, type) + self.pressed = False + self.door = None + print("Plate created!") + + # I should do the check if the value is boolean, but I do not want to + # (all of my methods do not have such checks, which is a bit ugh) + def set_door(self, door): + self.door = door + + def set_pressed(self, value): + self.pressed = value + if self.door is None: + raise ValueError("Door is None") + + self.door.set_open(self.pressed) + + def is_pressed(self): + return self.pressed + +# ---------------------------------------------------------------------- +# Class Door +# ---------------------------------------------------------------------- +class Door(MapElement): + def __init__(self, col, row, type): + super().__init__(col, row, type) + self.open = False + print("Door created") + + def set_open(self, value): + self.open = value + + def is_open(self): + return self.open + + # I have to ovverride the sprite method, because I have to check if it is opened + def draw(self, screen): + if self.open == False: + super().draw(screen) + else: + pass + +# ---------------------------------------------------------------------- +# Class GrabbableObject +# ---------------------------------------------------------------------- +# represents objects that can be grabbed and put in inventory +# they will be necessary to press certain types of pressure plates +# yet to be implemented +# ---------------------------------------------------------------------- +class GrabbableObject(MapElement): + def __init__(self, col, row, type): + super().__init__(col, row, type) + self.grabbed = False + + def grab(self): + self.grabbed = True + + def release(self): + self.grabbed = False + + def is_grabbed(self): + return self.grabbed + + # I have to ovverride the sprite method, because I have to check if it is opened + def draw(self, screen): + if self.grabbed == False: + super().draw(screen) + else: + pass \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..93279bc --- /dev/null +++ b/main.py @@ -0,0 +1,201 @@ +from datetime import timedelta +import pygame + +# project modules +from constants import * +from levelstate import Level +from levelstate import LevelState +from new_gui import * +import db_utils +from gamestate import * +from levelstate import Direction + +# ----------------------------------------------------------------------------------- +# Play state Loop +# ----------------------------------------------------------------------------------- +def play(game_state, events): + + # for ease of use + levelstate = game_state.level_state + player = levelstate.player + + # Event polling + for event in events: + if event.type == pygame.QUIT: + game_state.running = False + + elif event.type == pygame.KEYDOWN: + # Controls + if event.key == pygame.K_LCTRL: + player.change_power() + if event.key == pygame.K_LSHIFT: + levelstate.change_player() + player = levelstate.player + if event.key == pygame.K_ESCAPE: + game_state.state = State.PAUSE + game_state.pause_start = pygame.time.get_ticks() + + # Movement + if event.key == pygame.K_a: + player.x = levelstate.try_move(Direction.HORIZ, player.x, player.y, -player.speed) + elif event.key == pygame.K_d: + player.x = levelstate.try_move(Direction.HORIZ, player.x, player.y, player.speed) + elif event.key == pygame.K_w: + player.y = levelstate.try_move(Direction.VERT, player.x, player.y, -player.speed) + elif event.key == pygame.K_s: + player.y = levelstate.try_move(Direction.VERT, player.x, player.y, player.speed) + + # update level state + levelstate.update() + + # update sprites + levelstate.all_sprites.update() + + # bg + levelstate.draw(screen) + + # Timer Text + time = timedelta(milliseconds=game_state.elapsed) + levelstate.draw_gui(screen, time, small_font) + + # rendering of all the sprites + levelstate.all_sprites.draw(screen) + +# ----------------------------------------------------------------------------------- +# Pause state Loop +# ----------------------------------------------------------------------------------- +def pause_menu(game_state, events, mouse): + # getting menu object + menu = game_state.pause_menu + + # Display the menu + menu.draw(screen, mouse, title_font, small_font) + + # Event polling handled by the menu + for event in events: + value = menu.handle_event(event, mouse) + + # Actions based on what has been done in the interaction with the menu + if value == "playing": + game_state.state = State.PLAY + game_state.paused_time += pygame.time.get_ticks() - game_state.pause_start + game_state.pause_start = None + elif value == "select_level": + game_state.level_state = None + game_state.state = State.SELECT + elif value == "quit_game": + game_state.running = False + +# ----------------------------------------------------------------------------------- +# Menu state Loop +# ----------------------------------------------------------------------------------- +def start_menu(game_state, events, mouse): + menu = game_state.start_menu + + menu.draw(screen, mouse, title_font, small_font) + + for event in events: + value = menu.handle_event(event, mouse) + if value == "quit_game": + game_state.running = False + elif value == "select_level": + game_state.state = State.SELECT + elif value == "delete": + db_utils.reset_db() + +# ----------------------------------------------------------------------------------- +# Select state Loop +# ----------------------------------------------------------------------------------- +def select_level(game_state, events, mouse): + menu = game_state.select_menu + + menu.draw(screen, mouse, title_font, small_font) + + # Event polling + for event in events: + value = menu.handle_event(event, mouse) + if value == "quit_game": + game_state.running = False + elif value == "menu": + game_state.state = State.MENU + # The select level buttons have 2 values, the path and if the level is unlocked + # This is way there is the check for a tuple + elif value != None and isinstance(value, tuple): + completed = int(value[1]) + level_path = value[0] + if completed == 1: + game_state.level_state = LevelState(Level(level_path)) + game_state.state = State.PLAY + # Start timer + game_state.play_start = pygame.time.get_ticks() + game_state.paused_time = 0 + game_state.elapsed = 0 + break + +# ----------------------------------------------------------------------------------- +# Complete state Loop +# ----------------------------------------------------------------------------------- +def completed_menu(game_state, events, mouse): + menu = game_state.completed_menu + + menu.draw(screen, mouse, title_font, small_font) + for event in events: + value = menu.handle_event(event, mouse) + + if value == "quit_game": + game_state.running = False + elif value == "select_level": + game_state.level_state = None + game_state.state = State.SELECT + elif value == "retry": + game_state.state = State.PLAY + game_state.level_state = LevelState(game_state.level_state.level) + # Start timer + game_state.play_start = pygame.time.get_ticks() + game_state.paused_time = 0 + game_state.elapsed = 0 + + +# ----------------------------------------------------------------------------------- +# PYGAME SETUP +# ----------------------------------------------------------------------------------- +pygame.init() +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +clock = pygame.time.Clock() + +# Graphic assets +title_font = pygame.font.Font('assets/ARCO.ttf', 35) +small_font = pygame.font.Font('assets/ARCO.ttf', 20) +background = pygame.transform.scale_by(pygame.image.load('assets/background.png').convert_alpha(), SCALE_FACTOR) + +# Db Init +db_utils.init_db() + +# GameState setup +game_state = GameState() + +# MAIN LOOP +while game_state.running: + screen.blit(background, (0, 0)) + events = pygame.event.get() + mouse = pygame.mouse.get_pos() + + if game_state.state == State.MENU: + start_menu(game_state, events, mouse) + elif game_state.state == State.PLAY: + play(game_state, events) + elif game_state.state == State.PAUSE: + pause_menu(game_state, events, mouse) + elif game_state.state == State.SELECT: + select_level(game_state, events, mouse) + elif game_state.state == State.COMPLETE: + completed_menu(game_state, events, mouse) + + # update game_state + game_state.update(pygame.time.get_ticks()) + + pygame.display.flip() + clock.tick(FPS) + +pygame.quit() + diff --git a/new_gui.py b/new_gui.py new file mode 100644 index 0000000..574ce49 --- /dev/null +++ b/new_gui.py @@ -0,0 +1,307 @@ +# libraries +from enum import Enum +from functools import lru_cache +import pygame + +# project modules +from constants import * + +# NOTE_1 +# Refactored using AI (Claude), did a great job, uses some obscure syntax +# still much better than the original which was becoming unusable +# I will use some of the stuff introduced by AI in this file to refactor all the other files + +# NOTE_2 +# The .get() function takes 2 values, the first is the field of the dictionary, the second +# is a default value to return if the key is not found + +# ------------------------------------------------------------- +# Enum and tables +# ------------------------------------------------------------- + +class ButtonTypes(Enum): + # The value doubles as the asset prefix: assets/_btn.png + # and assets/_dark_btn.png + PLAY = "play" + RESUME = "resume" + MENU = "menu" + DELETE = "delete" + QUIT = "quit" + RETRY = "retry" + FW = "fw" + BW = "bw" + GO = "go" + + +class MenuType(Enum): + PAUSE = 0 + COMPLETED = 1 + START = 2 + SELECT = 3 + + +class EntryState(Enum): + UNLOCKED = 0 + LOCKED = 1 + + +# Unscaled hitbox sizes (width, height); anything not listed uses the default +DEFAULT_HITBOX = (232, 82) # Defaults to a normal button +HITBOX_SIZES = { + ButtonTypes.FW: (105, 82), + ButtonTypes.BW: (105, 82), + ButtonTypes.GO: (83, 63), +} + +CARD_FILES = { + MenuType.PAUSE: "assets/paused_card.png", + MenuType.COMPLETED: "assets/completed_card.png", + MenuType.START: "assets/menu_card.png", + MenuType.SELECT: "assets/select_card.png", +} + +ICON_FILES = { + EntryState.LOCKED: "assets/locked.png", + EntryState.UNLOCKED: "assets/unlocked.png", +} + +# Layout constants for SelectEntry +ENTRY_WIDTH = 500 +ENTRY_PADDING = 20 +ENTRY_ICON_X = 800 + + +# -------------------------------------------------------------------------- +# Helpers +# -------------------------------------------------------------------------- + +@lru_cache(maxsize=None) +def load_image(path, scale=1): + image = pygame.image.load(path).convert_alpha() + if scale != 1: + image = pygame.transform.scale_by(image, scale) + return image + + +# -------------------------------------------------------------------------- +# Widgets +# -------------------------------------------------------------------------- + +class Widget: + # Anything a MenuPage can stack vertically. + + # Note: this notation is called type hinting + rect: pygame.Rect + + def place(self, centerx, y): + self.rect.centerx = centerx + self.rect.y = y + + # This are just the interface for the methods implemented in the various widgets + def height(self, text_font): + return self.rect.height + + def draw(self, screen, mouse, text_font): + raise NotImplementedError + + +class Button(Widget): + def __init__(self, value, kind=ButtonTypes.PLAY): + # value is a free-form field returned when the button is clicked + self.value = value + self.kind = kind + + # f-strings from python 3.6 to embed variables + prefix = f"assets/{kind.value}" + self.image = load_image(f"{prefix}_btn.png", SCALE_FACTOR) + self.dark_image = load_image(f"{prefix}_dark_btn.png", SCALE_FACTOR) + + width, height = HITBOX_SIZES.get(kind, DEFAULT_HITBOX) + self.rect = pygame.Rect(0, 0, int(width * SCALE_FACTOR), int(height * SCALE_FACTOR)) + + # text font is none because buttons have text in the image but the interface is the same for all widgets + def height(self, text_font=None): + return self.image.get_height() / 2 + + def clicked(self, mouse): + return self.rect.collidepoint(mouse) + + def draw(self, screen, mouse, text_font=None): + image = self.dark_image if self.clicked(mouse) else self.image + # The image is always centred on the hitbox, so nothing to keep in sync + screen.blit(image, image.get_rect(center=self.rect.center)) + + +class TextArea(Widget): + def __init__(self, text, color): + self.text = text + self.color = color + self.rect = pygame.Rect(0, 0, 0, 0) + + def height(self, text_font): + return text_font.size(self.text)[1] + + def draw(self, screen, mouse, text_font, anchor="midtop"): + # The anchor can be any rect attribute like midtop, topleft etc + surface = text_font.render(self.text, True, self.color) + position = getattr(self.rect, anchor) + screen.blit(surface, surface.get_rect(**{anchor: position})) + + +class SelectEntry(Widget): + # One row of the level-select menu: [go button] [label] ........ [lock icon] + + def __init__(self, text, value, state): + self.value = value + self.btn = Button(value, ButtonTypes.GO) + self.label = TextArea(text, DARK) + self.icon = load_image(ICON_FILES[state]) + self.icon_rect = self.icon.get_rect() + self.rect = pygame.Rect(0, 0, ENTRY_WIDTH, self.btn.rect.height) + + def place(self, centerx, y): + super().place(centerx, y) + x = self.rect.x + ENTRY_PADDING + self.btn.rect.topleft = (x, self.rect.y) + self.label.rect.midleft = (x + self.btn.rect.width * 3, self.rect.centery) + self.icon_rect.topleft = (ENTRY_ICON_X, self.rect.y) + + def clicked(self, mouse): + return self.btn.clicked(mouse) + + def draw(self, screen, mouse, text_font): + self.btn.draw(screen, mouse) + self.label.draw(screen, mouse, text_font, anchor="center") + screen.blit(self.icon, self.icon_rect) + + +# -------------------------------------------------------------------------- +# Menu page +# -------------------------------------------------------------------------- + +class MenuPage: + """One card with optional top text, buttons, entries and bottom text, + stacked vertically and centred on screen.""" + + def __init__(self, kind): + self.top_text_area = None + self.bottom_text_area = None + self.buttons = [] + self.entries = [] + self.total_height = 0 # set by draw(), read by Menu + self.bg_image = load_image(CARD_FILES[kind], SCALE_FACTOR) + + @property + def is_full(self): + return len(self.buttons) + len(self.entries) >= MAX_BTN + + def add_button(self, value, kind): + self.buttons.append(Button(value, kind)) + + def add_entry(self, text, value, state): + self.entries.append(SelectEntry(text, value, state)) + + def add_top_text(self, text): + self.top_text_area = TextArea(text, DARK) + + def add_bottom_text(self, text): + self.bottom_text_area = TextArea(text, DARK) + + def _elements(self): + elements = [] + if self.top_text_area: + elements.append(self.top_text_area) + elements.extend(self.buttons) + elements.extend(self.entries) + if self.bottom_text_area: + elements.append(self.bottom_text_area) + return elements + + def draw(self, screen, mouse, btn_font, text_font): + # btn_font is unused now; kept so existing calls don't break + center_x, center_y = screen.get_rect().center + screen.blit(self.bg_image, self.bg_image.get_rect(center=(center_x, center_y))) + + elements = self._elements() + spacing = BTN_HEIGHT * 0.5 + heights = [e.height(text_font) for e in elements] + self.total_height = sum(heights) + spacing * max(len(elements) - 1, 0) + + y = center_y - self.total_height / 2 + for element, height in zip(elements, heights): + element.place(center_x, y) + element.draw(screen, mouse, text_font) + y += height + spacing + + def handle_event(self, event, mouse): + if event.type == pygame.QUIT: + return "quit_game" + + if event.type == pygame.MOUSEBUTTONDOWN: + for clickable in (*self.buttons, *self.entries): + if clickable.clicked(mouse): + return clickable.value + return None + + +# -------------------------------------------------------------------------- +# Multi-page menu +# -------------------------------------------------------------------------- + +class Menu: + """Paged menu: new pages are created automatically when one is full. + For now pages hold only buttons or entries, no top/bottom text.""" + + def __init__(self, menu_type): + self.menu_type = menu_type + self.pages = [MenuPage(menu_type)] + self.current_page_i = 0 + + self.prev_btn = Button(None, ButtonTypes.BW) + self.menu_btn = Button("menu", ButtonTypes.MENU) + self.next_btn = Button(None, ButtonTypes.FW) + self.nav_buttons = (self.prev_btn, self.menu_btn, self.next_btn) + + @property + def page(self): + return self.pages[self.current_page_i] + + def _page_with_room(self): + if self.pages[-1].is_full: + self.pages.append(MenuPage(self.menu_type)) + return self.pages[-1] + + def add_button(self, value, kind): + self._page_with_room().add_button(value, kind) + + def add_entry(self, text, value, state): + self._page_with_room().add_entry(text, value, state) + + def draw(self, screen, mouse, btn_font, text_font): + center_x, center_y = screen.get_rect().center + self.page.draw(screen, mouse, btn_font, text_font) + + spacing = BTN_HEIGHT * 0.5 + bottom_y = center_y + self.page.total_height / 2 + spacing + + group_width = sum(b.rect.width for b in self.nav_buttons) + step = group_width / 2.5 + + # Left / middle / right, spaced `step` apart around the centre + for i, button in enumerate(self.nav_buttons): + button.place(center_x + (i - 1) * step, bottom_y) + button.draw(screen, mouse) + + def handle_event(self, event, mouse): + if event.type == pygame.MOUSEBUTTONDOWN: + if self.prev_btn.clicked(mouse): + self.current_page_i = max(self.current_page_i - 1, 0) + return None + if self.next_btn.clicked(mouse): + self.current_page_i = min(self.current_page_i + 1, len(self.pages) - 1) + return None + if self.menu_btn.clicked(mouse): + return self.menu_btn.value + + # Also handles pygame.QUIT and clicks on the page's own buttons + return self.page.handle_event(event, mouse) \ No newline at end of file diff --git a/player.py b/player.py new file mode 100644 index 0000000..b46056e --- /dev/null +++ b/player.py @@ -0,0 +1,66 @@ +# libraries +from enum import Enum +import pygame + +# project modules +from constants import * + +# ------------------------------------------------------------ +# Class Player +# - represents the player object +# ------------------------------------------------------------ + +class PlayerTypes(Enum): + BLUE = "blue_robot" + GREEN = "green_robot" + +class Powers(Enum): + PRESS = "press" + GRAB = "grab" + +class Player(pygame.sprite.Sprite): + def __init__(self, x, y, active, type=PlayerTypes.BLUE): + super().__init__() + + # Attributes + self.speed = 1 + self.width = 50 + self.height = 50 + self.x = x + self.y = y + self.active = active + + # Yet to see if useful + self.direction = "up" + + # Yet to find usage for grab + self.powers = [Powers.PRESS.value, Powers.GRAB.value] + self.current_power_i = 0 + self.current_power = self.powers[self.current_power_i] + + # Sprite image + self.active_sprite = pygame.image.load(f"assets/{type.value}.svg").convert_alpha() + self.inactive_sprite = pygame.image.load(f"assets/{type.value}_inactive.svg").convert_alpha() + self.image = pygame.transform.smoothscale(self.inactive_sprite, (TILE_SIZE, TILE_SIZE)) + + # Hitbox and position + self.rect = self.image.get_rect() + self.rect.topleft = (self.x*TILE_SIZE, self.y*TILE_SIZE) + + def update(self): + self.image = pygame.transform.smoothscale(self.inactive_sprite, (TILE_SIZE, TILE_SIZE)) + + if self.active: + self.image = pygame.transform.smoothscale(self.active_sprite, (TILE_SIZE, TILE_SIZE)) + + # update rect position + self.rect.x = self.x * TILE_SIZE + X_OFFSET + self.rect.y = self.y * TILE_SIZE + Y_OFFSET + + # Function to change current power + def change_power(self): + self.current_power_i += 1 + if self.current_power_i >= len(self.powers): + self.current_power_i = 0 + + self.current_power = self.powers[self.current_power_i] diff --git a/progress.db b/progress.db new file mode 100644 index 0000000..9059257 Binary files /dev/null and b/progress.db differ