From 8efb003a67dd0d3938e823c84dd019be539f4524 Mon Sep 17 00:00:00 2001 From: nfx Date: Sat, 21 Dec 2024 21:56:34 +0100 Subject: [PATCH] =?UTF-8?q?Transf=C3=A9rer=20les=20fichiers=20vers=20''?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lvl_edit.py | 138 +++++++++++++++++++++++++++++++++++++++++++++++----- main.py | 16 +++--- objects.py | 60 +++++++++++++++++++++-- 3 files changed, 191 insertions(+), 23 deletions(-) diff --git a/lvl_edit.py b/lvl_edit.py index ead6134..42a7137 100644 --- a/lvl_edit.py +++ b/lvl_edit.py @@ -6,43 +6,91 @@ import objects import json import pyxel +import pathlib CELL_SIZE = 8 GRID_X = 11 GRID_Y = 6 -WIN_X = CELL_SIZE*GRID_X -WIN_Y = CELL_SIZE*GRID_Y +WIN_X = CELL_SIZE * GRID_X +WIN_Y = CELL_SIZE * GRID_Y -def load_file(lvl: int) -> list: +def load_file_json(lvl: int) -> list[list[int]]: """ :param lvl: (int) The id of the level to load :return: (list) The loaded level from the file Reads the corresponding level file and returns its loaded content """ - file = open(f"lvl/{lvl}.json", "r", encoding="utf-8") + file = open(f"lvl_json/{lvl}.json", "r", encoding="utf-8") decoder = json.JSONDecoder() return decoder.decode(file.read()) +def parse_file(content: bytes) -> list[list[int]]: + """ + :param content: (bytes) The raw content of the level file + :return: (list) The level data converted into a table + """ + res = [] + mem = [] + for v in content: + if int(v) == 255: + res.append(mem) + mem = [] + else: + mem.append(int(v)) + return res + + +def load_template() -> list[list[int]]: + """ + :return: (list) The content of the template level file + Loads the template json file and returns its content + """ + file = open("lvl_template.dat", 'rb') + return parse_file(file.read()) + + +def load_file(lvl: int) -> list[list[int]]: + """ + :param lvl: (int) The id of the level to load + :return: (list) The loaded level from the file + Reads the corresponding level file and returns its loaded content + """ + file = open(f"lvl/{lvl}.dat", "rb") + return parse_file(file.read()) + + +def save_file(lvl: int, data: list): + """ + :param lvl: (int) The id of the level to save + :param data: (list) The data to encode and save + Saves the given data into a level binary + """ + content = b'' + for line in data: + content += bytes(line + [255]) + file = open(f"lvl/{lvl}.dat", "wb") + file.write(content) + file.close() + + def can_load(lvl: int) -> bool: """ :param lvl: (int) The id of the file to check for :return: (bool) Returns if the level file exists and can be loaded Tests if a level can be loaded, returns the result as a boolean """ - try: - file = load_file(lvl) - return len(file) == GRID_Y - except FileNotFoundError: - return False + path = pathlib.Path(f"lvl/{lvl}.dat") + return path.is_file() class LevelManager: """ This class is responsible for handling game data """ + def __init__(self): self.current_level = 0 self.cache = [] @@ -55,7 +103,11 @@ class LevelManager: if can_load(lvl): self.cache = load_file(lvl) self.current_level = lvl - print(f"editing level {lvl}") + # print(f"editing level {lvl}") + elif lvl > 0: + self.cache = load_template() + self.current_level = lvl + # print(f"editing level {lvl}") def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None: """ @@ -76,28 +128,69 @@ class LevelManager: new_player = new return new_player + def build_data_from_collection(self, collection: objects.Collection): + """ + :param collection: (Collection) The collection to build the level data from + Turns the collection into a level data table and stores it into the cache + """ + # Build an empty data + res = [[0 for _ in range(GRID_X)] for _ in range(GRID_Y)] + # Iterate through the collection's objects and store their id into the level data at their locations + for v in collection.get_objects(): + if isinstance(v, objects.MapObject): + res[v.position.Y][v.position.X] = v.ID + self.cache = res + class Game: """ This class manages the game runtime """ + def __init__(self): self.level = None # An instance of the LevelManager class self.main_collection = None # An instance of Collection, usually the main one self.editor_collection = None # An instance of Collection used by the editor and ignored while saving self.player = None # The current instance of the Player object self.cursor = objects.Cursor() + self.status = objects.EditText() self._drag = None # The current object being dragged in the editor self._dragging = False # Determines is the mouse is currently in the dragging state + self.place_object_id = 0 # The object placed upon right click. + self.current_preview_display = None # The object that is being rendered as a preview(for obj selection) + self.preview_last_changed = 0 # The frame on which the preview object has been last set + + def _set_preview_display(self, obj: objects.MapObject | None = None): + """ + :param obj: (MapObject | None) The object to display as a preview + Parents the provided object into the editor collection to use as a temporary preview + """ + if self.current_preview_display is not None: + self.editor_collection.remove(self.current_preview_display, True) + if obj is not None: + self.current_preview_display = obj + self.editor_collection.add(obj) + self.preview_last_changed = pyxel.frame_count + def _upd(self): if self.player: change_level = 0 # Update cursor position mx, my = pyxel.mouse_x, pyxel.mouse_y - self.cursor.position = objects.clamp_in_range(objects.V2(mx//CELL_SIZE, my//CELL_SIZE)) + self.cursor.position = objects.clamp_in_range(objects.V2(mx // CELL_SIZE, my // CELL_SIZE)) + + if pyxel.mouse_wheel == 1: # Next Element + self.place_object_id = objects.editor_next_number(self.place_object_id) + self._set_preview_display(objects.editor_preview_objects[self.place_object_id]) + elif pyxel.mouse_wheel == -1: # Previous Element + self.place_object_id = objects.editor_previous_number(self.place_object_id) + self._set_preview_display(objects.editor_preview_objects[self.place_object_id]) + + if pyxel.frame_count-self.preview_last_changed >= 30 and self.current_preview_display: + self._set_preview_display() # Remove temporary preview once timer's reached 30 frames if pyxel.btnp(pyxel.KEY_LEFT): # Level switch controls change_level = -1 @@ -107,11 +200,23 @@ class Game: obj = self.main_collection.get_at_position(self.cursor.position) if obj and obj.can_be_deleted: self.main_collection.remove(obj, True) + elif pyxel.btnp(pyxel.MOUSE_BUTTON_RIGHT): # Insert an object + constructor = objects.editor_place_elements[self.place_object_id] + new_elt = constructor() + new_elt.position = self.cursor.position + self.main_collection.add(new_elt) + self.main_collection.on_top(self.editor_collection) + elif pyxel.btnp(pyxel.KEY_S) and pyxel.btn(pyxel.KEY_SHIFT): # Save the level file + self.level.build_data_from_collection(self.main_collection) + save_file(self.level.current_level, self.level.cache) + self.status.set_text(f"Saved {self.level.current_level}") + # print(f"Level saved as level {self.level.current_level}") if change_level != 0: # Clear cache and load another level self._drag = None self._dragging = False self.level.load_level(self.level.current_level + change_level) + self.status.set_text(f"Editing {self.level.current_level}") self.main_collection.clear_all() new_player = self.level.load_collection_from_cache(self.main_collection) self.player = new_player @@ -125,6 +230,16 @@ class Game: elif self._drag: self._drag.position = self.cursor.position else: + if self._drag: + overlap = self.main_collection.get_at_position(self._drag.position, [self._drag]) + # The level data doesn't support multiple objects at the same location + if overlap and overlap.can_be_deleted: + # If the object it is overlapping with can be normally deleted, delete it + self.main_collection.remove(overlap, True) + elif overlap: + # If the object it is overlapping with cant be normally deleted, + # deletes the dragged object instead + self.main_collection.remove(self._drag, True) self._dragging = False self._drag = None @@ -151,6 +266,7 @@ class Game: self.main_collection = main_c self.editor_collection = editor_c editor_c.add(self.cursor) + editor_c.add(self.status) main_c.add(editor_c) pyxel.init(WIN_X, WIN_Y, title="ice_walker") pyxel.run(self._upd, self._display) diff --git a/main.py b/main.py index 83ce349..2b51234 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ import objects import json import pyxel -import pathlib +from os.path import exists CELL_SIZE = 8 GRID_X = 11 @@ -22,7 +22,7 @@ def load_file_json(lvl: int) -> list: :return: (list) The loaded level from the file Reads the corresponding level file and returns its loaded content """ - file = open(f"lvl/{lvl}.json", "r", encoding="utf-8") + file = open(f"lvl_json/{lvl}.json", "r", encoding="utf-8") decoder = json.JSONDecoder() return decoder.decode(file.read()) @@ -33,7 +33,7 @@ def load_file(lvl: int) -> list: :return: (list) The loaded level from the file Reads the corresponding level file and returns its loaded content """ - file = open(f"lvl_bin/{lvl}.dat", "rb") + file = open(f"lvl/{lvl}.dat", "rb") content = file.read() res = [] mem = [] @@ -52,8 +52,7 @@ def can_load(lvl: int) -> bool: :return: (bool) Returns if the level file exists and can be loaded Tests if a level can be loaded, returns the result as a boolean """ - path = pathlib.Path(f"lvl_bin/{lvl}.dat") - return path.is_file() + return exists(f"lvl/{lvl}.dat") class LevelManager: @@ -69,9 +68,12 @@ class LevelManager: :param lvl: (int) The id of the level to load Loads the specified level and stores it into the cache """ + print(f"Loading level {lvl}") if can_load(lvl): self.cache = load_file(lvl) self.current_level = lvl + else: + print("level doesn't exist") def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None: """ @@ -104,10 +106,6 @@ class Game: def _upd(self): if self.player: - if pyxel.btnp(pyxel.KEY_R): # Reset the current level - self.main_collection.clear_all() - self.player = self.level.load_collection_from_cache(self.main_collection) - direction = objects.V2() if pyxel.btnp(pyxel.KEY_Z): # Determine move direction direction.Y = -1 diff --git a/objects.py b/objects.py index 4f1b5ad..6ee4a94 100644 --- a/objects.py +++ b/objects.py @@ -18,6 +18,7 @@ WIN_Y = CELL_SIZE*GRID_Y class V2: """ This object represents a grid point + It is not meant to be mutable """ def __init__(self, x=0, y=0): self.X = x # The horizontal position of the object @@ -195,9 +196,9 @@ class Collection: res.append(v) return res - def on_top(self, obj: MapObject): + def on_top(self, obj): """ - :param obj: (MapObject) the object to put in front + :param obj: (MapObject | Collection) the object to put in front Changes the order in the Collection's internal list so the provided object gets rendered last(and on top of everything else) """ @@ -236,7 +237,6 @@ class Collection: if isinstance(v, MapObject): v.collection = None self._instances = [] - self.exits = 0 class Switch(MapObject): @@ -355,6 +355,27 @@ class Cursor(MapObject): pyxel.circb(self.position.X * CELL_SIZE + 4, self.position.Y * CELL_SIZE + 4, 2, 4) +class EditText(MapObject): + def __init__(self): + super().__init__() + self.ID = -2 + self._txt = "" + self._frame_count = 0 + self.frame_limit = 30 + + def draw(self): + if len(self._txt) > 0: + self._frame_count += 1 + if self._frame_count > self.frame_limit: + self._txt = "" + return + pyxel.text(self.position.X * CELL_SIZE, self.position.Y * CELL_SIZE, self._txt, 7, None) + + def set_text(self, txt: str): + self._txt = txt + self._frame_count = 0 + + # Wrapping the above classes into a list for the loader to use loadable_elements = [ @@ -376,3 +397,36 @@ def get_type_from_id(element_id: int): if v().ID == element_id: return v return None + + +# Gives the editor the elements that can be placed using right mouse button +editor_place_elements = [] +editor_preview_objects = [] + +for element in loadable_elements: + prev_object = element() + if prev_object.can_be_placed: + editor_place_elements.append(element) + editor_preview_objects.append(prev_object) + + +def editor_next_number(current: int) -> int: + """ + :param current: (int) The current index used by the editor + :return: (int) The next index of the editor_place_elements list + Used by the editor to determine the next element that can be placed using the mouse wheel + """ + if current >= len(editor_place_elements)-1: + return 0 + return current + 1 + + +def editor_previous_number(current: int) -> int: + """ + :param current: (int) The current index used by the editor + :return: (int) The previous index of the editor_place_elements list + Used by the editor to determine the previous element that can be placed using the mouse wheel + """ + if current <= 0: + return len(editor_place_elements) - 1 + return current - 1