Browse Source

Transférer les fichiers vers ''

master
nfx 4 weeks ago
parent
commit
8efb003a67
  1. 132
      lvl_edit.py
  2. 16
      main.py
  3. 60
      objects.py

132
lvl_edit.py

@ -6,6 +6,7 @@
import objects import objects
import json import json
import pyxel import pyxel
import pathlib
CELL_SIZE = 8 CELL_SIZE = 8
GRID_X = 11 GRID_X = 11
@ -15,34 +16,81 @@ WIN_X = CELL_SIZE*GRID_X
WIN_Y = CELL_SIZE * GRID_Y 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 :param lvl: (int) The id of the level to load
:return: (list) The loaded level from the file :return: (list) The loaded level from the file
Reads the corresponding level file and returns its loaded content 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() decoder = json.JSONDecoder()
return decoder.decode(file.read()) 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: def can_load(lvl: int) -> bool:
""" """
:param lvl: (int) The id of the file to check for :param lvl: (int) The id of the file to check for
:return: (bool) Returns if the level file exists and can be loaded :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 Tests if a level can be loaded, returns the result as a boolean
""" """
try: path = pathlib.Path(f"lvl/{lvl}.dat")
file = load_file(lvl) return path.is_file()
return len(file) == GRID_Y
except FileNotFoundError:
return False
class LevelManager: class LevelManager:
""" """
This class is responsible for handling game data This class is responsible for handling game data
""" """
def __init__(self): def __init__(self):
self.current_level = 0 self.current_level = 0
self.cache = [] self.cache = []
@ -55,7 +103,11 @@ class LevelManager:
if can_load(lvl): if can_load(lvl):
self.cache = load_file(lvl) self.cache = load_file(lvl)
self.current_level = 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: def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None:
""" """
@ -76,21 +128,52 @@ class LevelManager:
new_player = new new_player = new
return new_player 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: class Game:
""" """
This class manages the game runtime This class manages the game runtime
""" """
def __init__(self): def __init__(self):
self.level = None # An instance of the LevelManager class self.level = None # An instance of the LevelManager class
self.main_collection = None # An instance of Collection, usually the main one 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.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.player = None # The current instance of the Player object
self.cursor = objects.Cursor() self.cursor = objects.Cursor()
self.status = objects.EditText()
self._drag = None # The current object being dragged in the editor 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._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): def _upd(self):
if self.player: if self.player:
change_level = 0 change_level = 0
@ -99,6 +182,16 @@ class Game:
mx, my = pyxel.mouse_x, pyxel.mouse_y 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 if pyxel.btnp(pyxel.KEY_LEFT): # Level switch controls
change_level = -1 change_level = -1
elif pyxel.btnp(pyxel.KEY_RIGHT): elif pyxel.btnp(pyxel.KEY_RIGHT):
@ -107,11 +200,23 @@ class Game:
obj = self.main_collection.get_at_position(self.cursor.position) obj = self.main_collection.get_at_position(self.cursor.position)
if obj and obj.can_be_deleted: if obj and obj.can_be_deleted:
self.main_collection.remove(obj, True) 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 if change_level != 0: # Clear cache and load another level
self._drag = None self._drag = None
self._dragging = False self._dragging = False
self.level.load_level(self.level.current_level + change_level) 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() self.main_collection.clear_all()
new_player = self.level.load_collection_from_cache(self.main_collection) new_player = self.level.load_collection_from_cache(self.main_collection)
self.player = new_player self.player = new_player
@ -125,6 +230,16 @@ class Game:
elif self._drag: elif self._drag:
self._drag.position = self.cursor.position self._drag.position = self.cursor.position
else: 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._dragging = False
self._drag = None self._drag = None
@ -151,6 +266,7 @@ class Game:
self.main_collection = main_c self.main_collection = main_c
self.editor_collection = editor_c self.editor_collection = editor_c
editor_c.add(self.cursor) editor_c.add(self.cursor)
editor_c.add(self.status)
main_c.add(editor_c) main_c.add(editor_c)
pyxel.init(WIN_X, WIN_Y, title="ice_walker") pyxel.init(WIN_X, WIN_Y, title="ice_walker")
pyxel.run(self._upd, self._display) pyxel.run(self._upd, self._display)

16
main.py

@ -6,7 +6,7 @@
import objects import objects
import json import json
import pyxel import pyxel
import pathlib from os.path import exists
CELL_SIZE = 8 CELL_SIZE = 8
GRID_X = 11 GRID_X = 11
@ -22,7 +22,7 @@ def load_file_json(lvl: int) -> list:
:return: (list) The loaded level from the file :return: (list) The loaded level from the file
Reads the corresponding level file and returns its loaded content 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() decoder = json.JSONDecoder()
return decoder.decode(file.read()) return decoder.decode(file.read())
@ -33,7 +33,7 @@ def load_file(lvl: int) -> list:
:return: (list) The loaded level from the file :return: (list) The loaded level from the file
Reads the corresponding level file and returns its loaded content 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() content = file.read()
res = [] res = []
mem = [] mem = []
@ -52,8 +52,7 @@ def can_load(lvl: int) -> bool:
:return: (bool) Returns if the level file exists and can be loaded :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 Tests if a level can be loaded, returns the result as a boolean
""" """
path = pathlib.Path(f"lvl_bin/{lvl}.dat") return exists(f"lvl/{lvl}.dat")
return path.is_file()
class LevelManager: class LevelManager:
@ -69,9 +68,12 @@ class LevelManager:
:param lvl: (int) The id of the level to load :param lvl: (int) The id of the level to load
Loads the specified level and stores it into the cache Loads the specified level and stores it into the cache
""" """
print(f"Loading level {lvl}")
if can_load(lvl): if can_load(lvl):
self.cache = load_file(lvl) self.cache = load_file(lvl)
self.current_level = lvl self.current_level = lvl
else:
print("level doesn't exist")
def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None: def load_collection_from_cache(self, collection: objects.Collection) -> objects.Player | None:
""" """
@ -104,10 +106,6 @@ class Game:
def _upd(self): def _upd(self):
if self.player: 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() direction = objects.V2()
if pyxel.btnp(pyxel.KEY_Z): # Determine move direction if pyxel.btnp(pyxel.KEY_Z): # Determine move direction
direction.Y = -1 direction.Y = -1

60
objects.py

@ -18,6 +18,7 @@ WIN_Y = CELL_SIZE*GRID_Y
class V2: class V2:
""" """
This object represents a grid point This object represents a grid point
It is not meant to be mutable
""" """
def __init__(self, x=0, y=0): def __init__(self, x=0, y=0):
self.X = x # The horizontal position of the object self.X = x # The horizontal position of the object
@ -195,9 +196,9 @@ class Collection:
res.append(v) res.append(v)
return res 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 Changes the order in the Collection's internal list so the provided object gets rendered last(and on top
of everything else) of everything else)
""" """
@ -236,7 +237,6 @@ class Collection:
if isinstance(v, MapObject): if isinstance(v, MapObject):
v.collection = None v.collection = None
self._instances = [] self._instances = []
self.exits = 0
class Switch(MapObject): 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) 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 # Wrapping the above classes into a list for the loader to use
loadable_elements = [ loadable_elements = [
@ -376,3 +397,36 @@ def get_type_from_id(element_id: int):
if v().ID == element_id: if v().ID == element_id:
return v return v
return None 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

Loading…
Cancel
Save