""" LEVEL EDITOR(prototype) The file that allows to create and edit levels THIS PROGRAM IS DEPRECATED, USE LVL_EDIT.PY TO EDIT LEVELS """ import pyxel import json import shutil CELL_SIZE = 20 GRID_X = 11 GRID_Y = 6 WIN_X = CELL_SIZE*GRID_X WIN_Y = CELL_SIZE*GRID_Y def clamp(n: int, a: int, b: int): """ :param n: The number to be clamped :param a: The minimum limit :param b: The maximum limit :return: The clamped number Returns the provided number n clamped between a and b """ if n < a: return a if n > b: return b return n def get_file_content(lvl: int) -> list: """ :param lvl: (int) The id of the file to read :return: (list) The level content of the file Reads a level file content and returns its data """ file = open(f"lvl/{lvl}.json", "r", encoding="utf-8") decoder = json.JSONDecoder() content = decoder.decode(file.read()) file.close() return content def can_load(lvl: int) -> bool: """ :param lvl: (int) The id of the file to check for :return: Returns if the level file exists and can be loaded """ try: file = get_file_content(lvl) return len(file) == GRID_Y except FileNotFoundError: return False class LevelManager: def __init__(self): self.level = 0 # the id of the currently loaded stage self.cache = [] # the current stage state is stored there self.tileTypes = { # helps with tile identifiers "empty": 0, "wall": 1, "player": 3, "exit": 2, "switch_blue": 4, "s_wall_blue": 5 } def load(self, lvl: int): """ :param lvl: (int) the level id Reloads the game using the specified level """ self.level = lvl self.cache = get_file_content(lvl) def save(self, lvl: int): """ :param lvl: (int) the level id Saves the cache under the specified level id """ file = open(f"lvl/{lvl}.json", "w", encoding="utf-8") encoder = json.JSONEncoder() file.write(encoder.encode(self.cache)) file.close() def update_tile(self, position: tuple[int, int], val: int): """ :param position: (tuple) The position of the tile to modify :param val: (int) The new value of the tile Updates the specified tile from the cache to the specified value """ self.cache[position[1]][position[0]] = val def get_tile(self, position: tuple[int, int]) -> int: """ :param position: (tuple) the position of the requested tile :return: (int) the value of the tile Fetches the current value of the specified tile or -1 if out of range """ if not self.is_in_range(position): return -1 return self.cache[position[1]][position[0]] def is_in_range(self, position: tuple[int, int]) -> bool: """ :param position: (tuple) The position that will be checked :return: (bool) indicates whether the said position is in map bounds Checks if the given position is within the map range(a level should have been loaded first) """ if len(self.cache) == 0: return False if position[0] < 0 or position[1] < 0: return False if position[1] > GRID_Y-1: return False if position[0] > GRID_X-1: return False return True def move_tile(self, tile_a, tile_b): """ :param tile_a: (tuple) The position of the tile to be moved :param tile_b: (tuple) The position of the target tile Moves the content of tile_a to tile_b(overwrites tile_b and sets tile_a to 0) This is meant for player movement """ if tile_a != tile_b: content = self.get_tile(tile_a) self.update_tile(tile_b, content) self.update_tile(tile_a, self.tileTypes['empty']) def find_object(self, elt: int) -> tuple[int, int]: """ :param elt: (int) The type of element to find on the map(use self.tileTypes) :return: (tuple) The position of the tile(or (-1, -1) if not found) Finds the first instance of the provided element on the board, and returns its position """ for y in range(GRID_Y): for x in range(GRID_X): if self.cache[y][x] == elt: return x, y return -1, -1 def find_player(self): # finds the player in the stage grid and returns its coordinates return self.find_object(self.tileTypes['player']) def input_cmd(): cmd_txt = input("cmd => ").split(" ") if cmd_txt[0] == "load" and len(cmd_txt) > 1 and cmd_txt[1].isnumeric() and can_load(int(cmd_txt[1])): print(f"Now editing level {cmd_txt[1]}") runner.level.load(int(cmd_txt[1])) elif cmd_txt[0] == "create" and len(cmd_txt) > 1 and cmd_txt[1].isnumeric(): print(f"Now editing level {cmd_txt[1]}") shutil.copyfile("lvl_template.json", f'lvl/{cmd_txt[1]}.json') runner.level.load(int(cmd_txt[1])) elif cmd_txt[0] == "save": runner.level.save(runner.level.level) print(f"Saved successfully") else: print("Invalid Command") class Editor: def __init__(self): self.level = LevelManager() # An instance that manages level data and navigating self.isInit = False # Safety attribute that prevents init from running twice self.cursor = (0, 0) # Describes the mouse position(snapped to grid) self.toolToggle = 0 self.toolToggleRotation = [ self.level.tileTypes['wall'], self.level.tileTypes['exit'], self.level.tileTypes['switch_blue'], self.level.tileTypes['s_wall_blue'] ] self.element_display = [ "empty", "wall", "exit", "player", "blue switch", "blue switch wall" ] def on_click(self): # toggles the presence of an element at cursor position tile = self.level.get_tile(self.cursor) if tile != self.toolToggleRotation[self.toolToggle] and tile != self.level.tileTypes['player']: self.level.update_tile(self.cursor, self.toolToggleRotation[self.toolToggle]) elif tile != self.level.tileTypes['player']: self.level.update_tile(self.cursor, self.level.tileTypes['empty']) def move_player(self): # moves the player to the cursor(overwrites said tile) if self.cursor[0] >= 0 and self.cursor[1] >= 0: plr_pos = self.level.find_player() self.level.move_tile(plr_pos, self.cursor) def tool_rotation(self): # Changes the element that will be placed on click next_index = self.toolToggle + 1 if next_index >= len(self.toolToggleRotation): self.toolToggle = 0 else: self.toolToggle = next_index new_element = self.toolToggleRotation[self.toolToggle] print(f"Now placing {self.element_display[new_element]}") # manages controls and tools def upd(self): mx, my = pyxel.mouse_x, pyxel.mouse_y self.cursor = (mx//CELL_SIZE, my//CELL_SIZE) if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT): self.on_click() elif pyxel.btnp(pyxel.KEY_E): self.move_player() elif pyxel.btnp(pyxel.KEY_A): self.tool_rotation() elif pyxel.btnp(pyxel.KEY_C): input_cmd() # utility methods that draws the cursor to the mouse position def draw_cursor(self): x, y = self.cursor center_x = x*CELL_SIZE+CELL_SIZE//2 center_y = y*CELL_SIZE+CELL_SIZE//2 pyxel.circb(center_x, center_y, 5, 4) # self-explanatory, manages what's seen on screen def display(self): pyxel.cls(0) for y in range(GRID_Y): for x in range(GRID_X): tile = self.level.get_tile((x, y)) if tile == self.level.tileTypes['wall']: pyxel.rect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE, 7) elif tile == self.level.tileTypes['exit']: pyxel.rect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE, 11) elif tile == self.level.tileTypes['player']: pyxel.circ(x*CELL_SIZE+CELL_SIZE//2, y*CELL_SIZE+CELL_SIZE//2, CELL_SIZE//2-1, 8) elif tile == self.level.tileTypes['switch_blue']: pyxel.circ(x*CELL_SIZE+CELL_SIZE//2, y*CELL_SIZE+CELL_SIZE//2, CELL_SIZE//3-1, 12) elif tile == self.level.tileTypes['s_wall_blue']: pyxel.rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE, 12) self.draw_cursor() def init(self, lvl: int): # opens the editor window and loads the specified stage if not self.isInit: self.isInit = True self.level.load(lvl) pyxel.init(WIN_X, WIN_Y, title="ice_walker editor") pyxel.mouse(True) pyxel.run(self.upd, self.display) def start_editor(): runner.init(int(lvl_id)) if __name__ == "__main__": lvl_id = input("Please enter the id of the level to load: ") if lvl_id.isnumeric(): print(f"Now editing level {lvl_id}") runner = Editor() start_editor() else: print("Invalid level id provided, please rerun and try again")