""" ICE WALKER LEVEL EDITOR Runs a level editor which can be used to create and edit levels """ 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 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_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 """ 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 = [] def load_level(self, lvl: int): """ :param lvl: (int) The id of the level to load Loads the specified level and stores it into the cache """ if can_load(lvl): self.cache = load_file(lvl) self.current_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: """ :param collection: (Collection) The collection to load the objects into :return: The player object created by this method Loads the level elements using the loaded cache, returns the new player instance """ new_player = None for y in range(len(self.cache)): for x in range(len(self.cache[y])): element_id = self.cache[y][x] elt = objects.get_type_from_id(element_id) if elt: new = elt() new.position = objects.V2(x, y) collection.add(new) if element_id == 3: 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)) 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 elif pyxel.btnp(pyxel.KEY_RIGHT): change_level = 1 elif pyxel.btnp(pyxel.KEY_DELETE): # Object management controls 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 self.main_collection.add(self.editor_collection) return # Not handle the rest because not necessary if pyxel.btn(pyxel.MOUSE_BUTTON_LEFT): if not self._dragging: self._dragging = True self._drag = self.main_collection.get_at_position(self.cursor.position) 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 def _display(self): pyxel.cls(0) for v in self.main_collection.get_objects(): # Render all active objects if isinstance(v, objects.MapObject): v.draw() elif isinstance(v, objects.Collection) and v.visible: # Render embedded collections, if visible for embed_v in v.get_objects(): embed_v.draw() # Draw cursor pyxel.rect(pyxel.mouse_x, pyxel.mouse_y, 1, 1, 5) def run(self, lvl_m: LevelManager, main_c: objects.Collection, editor_c: objects.Collection): """ :param lvl_m: (LevelManager) The instance of LevelManager to assign with the game instance :param main_c: (Collection) The primary collection containing the entirety of the game :param editor_c: (Collection) The editor's collection Initializes and runs the editor """ self.level = lvl_m 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) if __name__ == "__main__": main_collection = objects.Collection() editor_collection = objects.Collection() level = LevelManager() # Load the first level of the game level.load_level(1) player = level.load_collection_from_cache(main_collection) game = Game() # Initializes the game engine game.player = player game.run(level, main_collection, editor_collection)