|
|
|
@ -6,6 +6,7 @@
|
|
|
|
|
import objects |
|
|
|
|
import json |
|
|
|
|
import pyxel |
|
|
|
|
import pathlib |
|
|
|
|
|
|
|
|
|
CELL_SIZE = 8 |
|
|
|
|
GRID_X = 11 |
|
|
|
@ -15,34 +16,81 @@ 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,21 +128,52 @@ 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 |
|
|
|
@ -99,6 +182,16 @@ class Game:
|
|
|
|
|
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): |
|
|
|
@ -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) |
|
|
|
|