You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

285 lines
11 KiB

"""
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)